안녕하세요!

이번에도 제가 프로그래밍을 하면서 겪은 문제와, 그 문제를 어떻게 해결했는지를 말씀드려볼까 합니다.


.NET에서는 데이터를 암호화하거나 복호화하는 기능이 정의된 클래스를 지원합니다.

관련된 형식은 모두 System.Security.Cryptography 네임스페이스에 정의되어 있는데요!


암/복호화를 지원하는 클래스는 여러 가지가 있지만, 가장 위엔 두 가지가 있습니다.

  • SymmetricAlgorithm (대칭 알고리즘)
  • AsymmetricAlgorithm (비대칭 알고리즘)

대칭 알고리즘의 경우, 키가 한 개 사용됩니다. 그 키를 데이터를 암/복호화하는데 사용하는 것이죠.
비대칭 알고리즘의 경우, 키가 두 개 사용됩니다. 각각 공개키, 개인키가 있는데요! A와 B라는 사용자가 있다면, A는 공개키로 데이터를 암호화하고, B는 자기가 가지고 있는 개인키로 데이터를 복호화하는 방식입니다. (A가 암호화한 데이터는 B가 가진 개인키로만 복호화가 가능합니다)

비대칭 알고리즘의 경우, 개인키와 공개키 한 쌍의 키가 있기 때문에 암호가 틀렸는지 알 수 있습니다. (대표적인 비대칭 알고리즘인 RSA는 인수분해를 이용하죠)
하지만, 대칭 알고리즘의 경우 암호가 틀렸는지 알 방법이 없습니다.


위 그림처럼 암호가 맞는지를 검증하는 부분이 있다면 참 편리할텐데, 암호가 맞는지 검증하는 부분이 도입된다면 그 알고리즘은 Brute-force 공격에 매우 취약해 지겠죠. 그래서인지 대칭 알고리즘은 데이터를 복호화할 때 입력된 키를 가지고 복호화를 시도합니다. 암호를 검사하지 않기 때문에 복호화의 결과가 전혀 다르게 나올 수도 있죠.. 심지어 .NET의 경우 대칭 알고리즘을 사용할 때 일치하지 않는 키가 사용되면 CryptographicException 예외가 발생합니다. 암호화 모드(CipherMode)를 기본 값이 아닌 다른 값으로 설정하면 예외가 발생하지 않는다는 글을 본 것 같은데.. 직접 테스트는 안해봐서 패스입니다.


데이터 암/복호화 - 잘못된 키를 사용하면? (C#)
using System;
using System.IO;
using System.Text;
using System.Security.Cryptography;

public class Program {
    public static void Main() {
        byte[] key1 = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };
        byte[] key2 = new byte[] { 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
        byte[] data = Encoding.ASCII.GetBytes("CSharp Programming");

        Console.WriteLine("Original : {0}", Convert.ToBase64String(data));
        Aes aes = Aes.Create();
        aes.Key = key1;

        MemoryStream ms = new MemoryStream();
        ICryptoTransform ct = aes.CreateEncryptor();

        using (CryptoStream cs = new CryptoStream(ms, ct, CryptoStreamMode.Write)) {
            cs.Write(data, 0, data.Length);
        }

        byte[] enc = ms.ToArray();
        Console.WriteLine("Encrypted: {0}", Convert.ToBase64String(enc));
        ct.Dispose();

        // 다른 키를 사용해서 복호화해보도록 하죠!
        aes.Key = key2;

        MemoryStream result = new MemoryStream();
        ms = new MemoryStream(enc);
        ct = aes.CreateDecryptor();

        using (CryptoStream cs = new CryptoStream(ms, ct, CryptoStreamMode.Read)) {
            byte[] read = new byte[64];
            int c = cs.Read(read, 0, 64);
            result.Write(read, 0, c);
        }

        Console.WriteLine("Decrypted: {0}", Convert.ToBase64String(result.ToArray()));
    }
}
예제 코드 (.NET Fiddle)


위 코드에서는 암호화와 복호화를 할 때 각각 다른 키(key1, key2)를 사용하고 있습니다.

.NET Fiddle에서는 Encrypted 까지는 출력이 잘 되다가 "[System.Security.Cryptography.CryptographicException: Padding is invalid and cannot be removed.]" 라는 오류가 출력됩니다.

한국어로 발생하는 예외 메세지는 "패딩이 잘못되었으며 제거할 수 없습니다." 입니다.



CryptographicException 예외가 발생했습니다.


참 웃긴 점은 이 예외가 발생하는 시점은 두 가지라는 것이죠.

  1. Read 메서드가 호출되는 시점. (데이터가 버퍼 크기보다 작거나 똑같아서 한 번에 복호화 작업이 끝나는 경우)
  2. Dispose 메서드가 호출되는 시점. (데이터가 버퍼 크기보다 커서 여러 번에 걸쳐야만 복호화 작업이 끝나는 경우)


그럼 1번의 경우에는 Read 메서드를 호출하는 부분을 try ~ catch 를 이용하여 감싸면 되는거 아니냐 생각하실 수도 있습니다.

try ~ catch 를 이용하여 예외를 무시한다 하더라도, using 을 사용했기 때문에 Dispose 메서드가 호출되고 다시 CryptographicException 이 발생하게 됩니다. 결국 1번이나 2번이나 결과적으로는 동일하게 예외가 발생하게 된다는 것이죠.


그럼 이 문제를 어떻게 해결하느냐.. 해결 방법은 세 가지입니다.

  1. using 자체를 try ~ catch 로 감싸는 방법
  2. CryptoStream 개체를 Dispose 하지 않는 방법
  3. Reflection 을 이용해서 내부 변수를 조작, TransformFinalBlock 메서드를 호출하지 않도록 하는 방법


이 방법 중 2번의 경우 메모리가 계속 쌓이게 됩니다. GC에서 처리가 되겠지만 권장하지 않습니다. 3번의 경우는 예외가 발생하는 시점 중 두 번째 경우에만 사용할 수 있기 때문에 데이터의 양이 적은 경우는 두 가지 처리를 해줘야 하는 번거로움이 있습니다. 남은 것은 1번입니다.


try ~ catch 로 감싸기
try {
    using (CryptoStream cs = new CryptoStream(ms, ct, CryptoStreamMode.Read)) {
        byte[] read = new byte[64];
        int c = cs.Read(read, 0, 64);
        result.Write(read, 0, c);
    }
} catch { }

이렇게 try ~ catch 로 코드를 감싼 후 catch 부분에서 처리를 해주는 방법이 있습니다.

더 나아가면 변수 하나를 선언하고, 특정 메서드를 호출할 때 변수의 값을 설정한 후 catch 부분에서 변수의 값을 검사하고 오류에 대한 정보를 반환/출력하는 형태로 개선할 수 있습니다.


이 방법은 임시 방편일 뿐입니다. 패딩이 잘못되었다는 것은 키가 잘못되었을 때에만 발생하는 것이 아니라 데이터가 잘못 되었을 경우에도 발생하는 예외이기 때문이죠. 그렇기 때문에 암호가 잘못된 것을 확인하는 정확한 방법은 비대칭 알고리즘을 사용하는 것이 최선이지만 대칭 알고리즘을 이용해야만 하는 경우에는 catch 부분에서 특별하게 처리를 해주면 됩니다. 오류 정보를 반환/출력할 때 "잘못된 키 혹은 데이터가 손상되었습니다" 정도로 처리해 줄 수 있겠죠.


네. 저는 저렇게 처리했습니다. 간단하잖아요!

짧게 쓴다는 글이 또 길어졌습니다. 긴 글 읽어주셔서 감사하고 저와 같은 문제에 직면한 분들께 도움이 되었으면 좋겠습니다.

'Trouble Shooting' 카테고리의 다른 글

클래스의 정적 메서드는 재정의할 수 없다.  (0) 2016.04.10

+ Recent posts