암호화의 이유

웹 애플리케이션을 개발하다 보면, 이메일 인증 링크, 비밀번호 재설정 토큰, 혹은 특정 식별자를 URL 파라미터로 전달해야 하는 경우가 있습니다. 이때, 데이터를 평문으로 노출하는 것은 보안상 매우 위험합니다.

이번에 개발하는 서비스에서 URL에 데이터를 담아서 보내줘야 하는 요구사항이 있어, 데이터를 암호화한 방법에 대해서 간단하게 기술합니다.

암호화 코드

아래의 코드는 이번에 사용한 암호화 방식의 개념만을 살려 구성한 코드 입니다.

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

internal class UriEncryptor
{
    // 반드시 32바이트(256비트) 키를 사용하세요.
    private static readonly string encryptionKey = "YourSecretKey";

    public static string Encrypt(string plainText)
    {
        byte[] clearBytes = Encoding.UTF8.GetBytes(plainText);
        using (Aes aes = Aes.Create())
        {
            aes.Key = Encoding.UTF8.GetBytes(encryptionKey.Substring(0, 32));
            aes.IV = new byte[16]; // 보안 강화를 위해 실제 환경에서는 고유한 IV를 생성해야 합니다.

            using (MemoryStream ms = new MemoryStream())
            {
                using (CryptoStream cs = new CryptoStream(ms, aes.CreateEncryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(clearBytes, 0, clearBytes.Length);
                    cs.Close();
                }
                // Base64로 인코딩하여 URL에 적합한 문자열로 변환
                return Uri.EscapeDataString(Convert.ToBase64String(ms.ToArray()));
            }
        }
    }

    public static string Decrypt(string cipherText)
    {
        string textToDecrypt = Uri.UnescapeDataString(cipherText);
        byte[] cipherBytes = Convert.FromBase64String(textToDecrypt);
        using (Aes aes = Aes.Create())
        {
            aes.Key = Encoding.UTF8.GetBytes(encryptionKey.Substring(0, 32));
            aes.IV = new byte[16];
            using (MemoryStream ms = new MemoryStream())
            {
                using (CryptoStream cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Write))
                {
                    cs.Write(cipherBytes, 0, cipherBytes.Length);
                    cs.Close();
                }
                return Encoding.UTF8.GetString(ms.ToArray());
            }
        }
    }
}

이제 이 코드에 담긴 개념들을 기술합니다.

대칭키 암호화와 AES

데이터를 특정 열쇠로 암호화 하고, 암호화된 데이터를 같은 열쇠로 복호화하여 원래의 데이터로 복구할 수 있는 방식을 대칭키(Symmetrict-key) 암호화 라고 합니다.

위 코드에서는 Aes.Create()를 호출하여 AES(Advanced Encryption Standard) 알고리즘을 사용하고 있습니다. AES는 전 세계적으로 가장 널리 쓰이는 암호화 표준으로, 처리 속도가 빠르고 보안성이 뛰어나 데이터를 암호화할 때 1순위로 고려되는 기술입니다.

256비트 암호화 키 (AES-256)

자물쇠의 핵심은 열쇠의 복잡도 입니다. AES 알고리즘은 열쇠(Key)의 길이에 따라 128비트, 192비트, 256비트로 나눠집니다. 숫자가 클수록 해커의 공격에 훨씬 강해집니다.

aes.Key = Encoding.UTF8.GetBytes(encryptionKey.Substring(0,32));

코드에서 Substring(0,32)를 통하여 정확히 문자열의 32바이트를 잘라서 키로 사용하고 있습니다. 1바이트는 8비트이므로, 32 * 8 = 256 bit가 됩니다. 즉, 이 코드는 현존하는 가장 강력한 수준의 AES-256 방식을 사용하여 데이터를 암호화하고 있습니다.

초기화 벡터 (IV - Initialization Vector)

비밀번호가 1234인 두 사용자가 있다고 가정해 봅시다. 만약 암호화된 결과물이 똑같이 ABCD로 나온다면, 해커는 암호를 풀지 못하더라도 이 두명은 같은 비밀번호를 쓰고 있다고 생각할 수 있습니다.

// 보안 강화를 위해 실제 환경에서는 고유한 IV를 생성해야 합니다.
aes.IV = new byte[16];

초기화 벡터 (IV)는 이런 패턴 분석을 위해 섞어주는 랜덤한 소금 같은 존재입니다. 같은 원본 데이터라도 IV가 다르면 완전히 다른 암호문이 생성됩니다.

현재 코드의 new byte[16]은 모두 0으로 채워진 고정 IV를 만듭니다. 실제 서비스에 배포할때는 aes.GenerateIV()를 사용해 매번 랜덤한 IV를 생성하고, 복호화할 때 사용할 수 있도록 암호문과 함께 전달하는 구조로 하면 더 좋습니다.

데이터 인코딩 (Base64)

암호화가 끝난 데이터는 컴퓨터만 이해할 수 있는 바이너리 byte 배열 입니다. 이것을 웹 브라우저나 JSON 데이터로 전달하려면 우리가 읽을 수 있는 text 형식으로 변환해야 합니다.

Convert.ToBase64String();

이때, 사용하는 것이 Base64 인코딩 입니다. 8비트 이진 데이터를 알파벳, 숫자, 그리고 일부 기호로 이루어진 아스키 문자열로 안전하게 포장해 주는 역할을 합니다.

URL 인코딩

Base64로 인코딩을 마쳤으니, 이제 URL 주소 뒤에 붙여서 바로 넣으면 될것처럼 보이지만, URL 에 존재하는 규칙 때문에, Base64에 포함될 수 있는 +, /, = 같은 기호들을 URL에 넣어도 문제가 발생하지 않게 만들어줘야 합니다. 예를들어 URL 에서 +는 공백으로 인식이 됩니다. 그대로 URL에 넣어서 서버로 전송하면, 서버가 데이터를 잘못 해석해서 복호화에 실패하게 됩니다.

따라서 Uri.EscapeDataString()을 사용하여 URL 규칙에 맞게 특수문자들을 %2B, %2F등의 형태로 한번 더 안전하게 변환해주어야 합니다. 복호화 할 때는 반대로 UnescapeDataString()을 먼저 거친 후에 처리합니다.

마치며

이메일 주소, 사번, 내부 식별자 등의 외부에 직접 노출되면 안 되는 파라미터가 있다면 위에서 소개한 암호화 방식들을 사용하여 외부 노출에 대한 리스크를 줄일 수 있습니다.