AES-CBC 방식으로 토큰 생성
이 문서는 AES-CBC 방식으로 토큰을 생성하는 방법을 설명합니다.
- 엘리스에서 설정한 "암호키"의 SHA256 값을 AES256 암호화 알고리즘의 key로 사용합니다.
- AES256의 IV값은 128-bit 랜덤 값을 사용합니다. (따라서 같은 내용이라도 암호화를 할 때 마다 다른 암호화 결과를 얻게 됩니다)
- AES256의 입력 데이터 길이는 IV 길이의 배수가 되어야 하므로, PKCS7 패딩 알고리즘을 이용해 128-bit의 배수로 맞춰줍니다.
- AES256+CBC에 위에서 구한 key와 IV값을 사용하여 패딩이 추가된 데이터를 암호화합니다.
- 복호화를 위해 IV값을 함께 전달할 필요가 있으므로, IV값과 암호화된 데이터를 순서대로 이어붙입니다. (가령, IV 가 asdf, 암호화된 데이터가 qwer 이면 결과값은 asdfqwer 이 됩니다.)
- 완성된 바이너리 토큰을 URL로 전달하기 쉽게 base64 인코딩을 적용해 문자열로 변환합니다.
- 추가적으로 필요에 따라 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
}
-
토큰 정보는 다음과 같은 JSON 문자열로 직렬화됩니다.
{"uid": "test-user-1", "fullname": "김토끼", "email": "tokki.kim@test.com", "ts": 1586961104518} -
AES256에 사용되는 Key 값은 UTF-8 인코딩된 문자열 "this_is_secret_key" 의 SHA256 해쉬 값을 사용하게 됩니다. 해 쉬 값은 바이너리 값이므로, 해당 값을 HEX 로 표시하면 다음과 같습니다.
7fa96cf6e1987fa29569acc71a2377cff0421aace8f15d8f165e06dc162f13f5 -
암호화 과정에서 IV 값은 매번 바뀌는 랜덤한 값이 사용되며, 이 때문에 같은 토큰 정보를 암호화해도 항상 다른 결과 토큰을 얻게 되고, 이는 보안을 유지하는데 도움이 됩니다. 이 예시에서 사용된 랜덤하게 생성된 IV 값은 다음과 같습니다. IV 값 역시 바이너리 값이므로, HEX 로 표시하였습니다.
acc90cc1b46ca2d3c6153e866a1a0b68 -
위의 Key 값과 IV 값을 이용해 직렬화된 토큰 정보를 암호화한 뒤, IV 와 암호화된 토큰을 이어붙여 Base64 인코딩하면 결과적으로 아래의 토큰을 얻게 됩니다.
rMkMwbRsotPGFT6GahoLaLySf3ppn+InnAuntavvhkBYwxo7t6XcvyZ2neAHTk4Va1cJsi/ckzZERar4QSSeTlbBUwpKGF2rQktcxrf+WerjVwlVyfDC6ge+zZZIlS+ZF794zrOX346tMLXxljX7kd2D4XwdifRvLpJaN7/Ij5c=