AES-CBC 방식으로 토큰 생성

이 문서는 AES-CBC 방식으로 토큰을 생성하는 방법을 설명합니다.

  1. 엘리스에서 설정한 "암호키"의 SHA256 값을 AES256 암호화 알고리즘의 key로 사용합니다.

  2. AES256의 IV값은 128-bit 랜덤 값을 사용합니다. (따라서 같은 내용이라도 암호화를 할 때 마다 다른 암호화 결과를 얻게 됩니다)

  3. AES256의 입력 데이터 길이는 IV 길이의 배수가 되어야 하므로, PKCS7 패딩 알고리즘을 이용해 128-bit의 배수로 맞춰줍니다.

  4. AES256+CBC에 위에서 구한 key와 IV값을 사용하여 패딩이 추가된 데이터를 암호화합니다.

  5. 복호화를 위해 IV값을 함께 전달할 필요가 있으므로, IV값과 암호화된 데이터를 순서대로 이어붙입니다. (가령, IV 가 asdf, 암호화된 데이터가 qwer 이면 결과값은 asdfqwer 이 됩니다.)

  6. 완성된 바이너리 토큰을 URL로 전달하기 쉽게 base64 인코딩을 적용해 문자열로 변환합니다.

  7. 추가적으로 필요에 따라 base64 문자열을 URL encoding 하여 전송합니다.

코드 예시

Python

암호화를 위해 다음 써드파티 라이브러리를 사용합니다.

표준 AES 암호화 알고리즘과 PKCS7 패딩을 사용하므로, 같은 알고리즘을 제공하는 다른 라이브러리를 사용하셔도 구현이 가능합니다.

import base64
import hashlib
import json
import os
import time

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

# 이 값은 예시를 위해 임의로 설정된 값입니다
CP_SECRET_KEY = '0123456789abcdefg'

def encrypt(content: str, secret_key: str) -> str:
    key = hashlib.sha256(secret_key.encode('utf-8')).digest()  # SH256를 통해 CP_SECRET_KEY를 256-bit Key로 변환합니다.
    iv = os.urandom(16)  # 16-byte (128-bit) IV를 사용합니다.
    padder = padding.PKCS7(128).padder()  # 128-bit PKCS7 padding을 사용합니다.

    backend = default_backend()  # 일반적으로 OpenSSL이 backend로 사용됩니다.
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend)  # CBC 모드의 AES를 사용합니다.
    encryptor = cipher.encryptor()

    content_padded = padder.update(content.encode('utf-8')) + padder.finalize()
    content_enc = encryptor.update(content_padded) + encryptor.finalize()

    return base64.b64encode(iv + content_enc).decode('utf-8')

token_info = {
    'uid': 'test-user-1',
    'fullname': '김토끼',
    'email': 'tokki.kim@test.com',
    'courseId': 1234,
    'ts': int(time.time() * 1000)
}

token = encrypt(json.dumps(token_info), CP_SECRET_KEY)

C#

using System;
using System.Text;
using System.Security.Cryptography;

static String Encrypt(String content, String secretKey)
{
    using (SHA256 sha256 = SHA256.Create())
    using (Aes aes = Aes.Create())
    {
        byte[] iv = new byte[16];
        rngCsp.GetBytes(iv);

        aes.Key = sha256.ComputeHash(Encoding.UTF8.GetBytes(secretKey));
        aes.IV = iv;
        aes.Mode = CipherMode.CBC;
        aes.Padding = PaddingMode.PKCS7;

        byte[] contentBytes = Encoding.UTF8.GetBytes(content);
        byte[] contentEnc = aes.CreateEncryptor()
            .TransformFinalBlock(contentBytes, 0, contentBytes.Length);

        byte[] result = new byte[iv.Length + contentEnc.Length];
        iv.CopyTo(result, 0);
        contentEnc.CopyTo(result, iv.Length);

        return Convert.ToBase64String(result);
    }
}

Java

import java.lang.System;
import java.security.MessageDigest;
import java.util.Base64;  // JDK 8+ only
import java.util.Random;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;  // JDK 6, JDK 7 only

static String encrypt(String content, String secretKey) throws Exception {
    byte[] iv = new byte[16];
    new Random().nextBytes(iv);

    MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
    sha256.update(secretKey.getBytes("UTF-8"));
    byte[] key = sha256.digest();

    // JAVA 에서 PKCS5Padding 는 실제로는 PKCS7Padding 로 작동합니다.
    Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
    aes.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));

    byte[] contentBytes = content.getBytes("UTF-8");
    byte[] contentEnc = aes.doFinal(contentBytes, 0, contentBytes.length);

    byte[] result = new byte[iv.length + contentEnc.length];
    System.arraycopy(iv, 0, result, 0, iv.length);
    System.arraycopy(contentEnc, 0, result, iv.length, contentEnc.length);

		// JDK 8+ only
    byte[] resultBase64 = Base64.getEncoder().encode(result);
    return new String(resultBase64);

		// // JDK 6, JDK 7
    // return DatatypeConverter.printBase64Binary(result);
}

토큰 암호화 단계별 예시

아래의 토큰 정보를 "this_is_secret_key" 를 암호화키로 AES-CBC 암호화하는 단계별 예시입니다.

{
    "uid": "test-user-1",
    "fullname": "김토끼",
    "email": "tokki.kim@test.com",
    "ts": 1586961104518
}
  1. 토큰 정보는 다음과 같은 JSON 문자열로 직렬화됩니다.

    {"uid": "test-user-1", "fullname": "김토끼", "email": "tokki.kim@test.com", "ts": 1586961104518}
  2. AES256에 사용되는 Key 값은 UTF-8 인코딩된 문자열 "this_is_secret_key" 의 SHA256 해쉬 값을 사용하게 됩니다. 해쉬 값은 바이너리 값이므로, 해당 값을 HEX 로 표시하면 다음과 같습니다.

    7fa96cf6e1987fa29569acc71a2377cff0421aace8f15d8f165e06dc162f13f5
  3. 암호화 과정에서 IV 값은 매번 바뀌는 랜덤한 값이 사용되며, 이 때문에 같은 토큰 정보를 암호화해도 항상 다른 결과 토큰을 얻게 되고, 이는 보안을 유지하는데 도움이 됩니다. 이 예시에서 사용된 랜덤하게 생성된 IV 값은 다음과 같습니다. IV 값 역시 바이너리 값이므로, HEX 로 표시하였습니다.

    acc90cc1b46ca2d3c6153e866a1a0b68
  4. 위의 Key 값과 IV 값을 이용해 직렬화된 토큰 정보를 암호화한 뒤, IV 와 암호화된 토큰을 이어붙여 Base64 인코딩하면 결과적으로 아래의 토큰을 얻게 됩니다.

    rMkMwbRsotPGFT6GahoLaLySf3ppn+InnAuntavvhkBYwxo7t6XcvyZ2neAHTk4Va1cJsi/ckzZERar4QSSeTlbBUwpKGF2rQktcxrf+WerjVwlVyfDC6ge+zZZIlS+ZF794zrOX346tMLXxljX7kd2D4XwdifRvLpJaN7/Ij5c=

Last updated