꽃미남 프로그래머 김포프가 창립한 탑 프로그래머 양성 교육 기관 POCU 아카데미 오픈!
절찬리에 수강생 모집 중!
프로그래밍 언어 입문서가 아닌 프로그래밍 기초 개념 입문서
문과생, 비전공자를 위한 프로그래밍 입문책입니다.
jobGuid 꽃미남 프로그래머 "Pope Kim"님의 이론이나 수학에 치우치지 않고 실무에 곧바로 쓸 수 있는 실용적인 셰이더 프로그래밍 입문서 #겁나친절 jobGuid "1판의내용"에 "새로바뀐북미게임업계분위기"와 "비자관련정보", "1판을 기반으로 북미취업에 성공하신 분들의 생생한 경험담"을 담았습니다.
Posted by 알 수 없는 사용자

이전의 내용은 다음의 링크에서 확인하시면 됩니다.

http://www.gamedevforever.com/141


  1. 빠르고 강력한 대칭키 방식의 암호

    대칭키 - 블록 암호는 구조가 단순하기 때문에 구현하기 쉽고, 비트열연산이 전부이므로 속도가 빠르다. 그에 비해 공개키 방식은 큰 수의 연산이 필수 이므로 큰 정수 연산을 수행하는 라이브러리가 필요하다. 결론적으로, 대칭키 방식의 암호는 비대칭키 - 공개키 방식의 암호에 비해 더 빠르다. 그 사실을 증명하기 위해 다음과 같은 두 코드를 작성 하였다. 두 코드 모두 OpenSSL에서 제공하는 crypto 라이브러리의 함수들을 사용하였다.

    AESSpeedTest.c

    #include <stdio.h>
    #include <stdlib.h>

    #include <openssl/evp.h>
    #include <openssl/aes.h>
    #include <openssl/err.h>

    int main(int argc, char *argv[])
    {
    uint8_t* plainText = (uint8_t*)malloc(sizeof(uint8_t) * 1024);
    uint8_t* cipherText = (uint8_t*)malloc(sizeof(uint8_t) * 1024 + EVP_MAX_BLOCK_LENGTH);
    uint8_t* key = (uint8_t*) "abcdefghijklmnop";
    unsigned int plainTextLen = 1024;

    EVP_CIPHER_CTX *ctx = (EVP_CIPHER_CTX*)malloc(sizeof(EVP_CIPHER_CTX));
    int paddingLen = 0, cyperLen = 0; register unsigned i;

    ERR_load_crypto_strings();
    EVP_CIPHER_CTX_init(ctx);

    if(EVP_EncryptInit(ctx, EVP_aes_128_ecb(), key, NULL) != 1)
    {
    printf("ERR : EVP_Encrypt() - %s\n", ERR_error_string(ERR_get_error(), NULL));
    return -1;
    }

    for(i = 0; i < 1024 * 1024; i++)
    {
    EVP_EncryptUpdate(ctx, cipherText, &cyperLen, plainText, plainTextLen);
    }

    if (EVP_EncryptFinal(ctx, cipherText + cyperLen, &paddingLen) != 1)
    {
    printf("ERR: EVP_EncryptFinal() - %s\n", ERR_error_string (ERR_get_error(), NULL));
    return -1;
    }

    EVP_CIPHER_CTX_cleanup(ctx);
    ERR_free_strings();

    return EXIT_SUCCESS;
    }


    RSASpeedTest.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    #include <openssl/rsa.h>
    #include <openssl/evp.h>
    int main(int argc, char *argv[])
    {
    RSA *rsa = RSA_generate_key(2048, 3, NULL, NULL);;
    unsigned char* plainText = (unsigned char*) "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345";
    uint8_t* cipherText;

    register unsigned int i;

    if (RSA_check_key(rsa) != 1)
    {
    perror("Failed to generate RSA key");
    exit(0);
    }

    cipherText = (uint8_t *) malloc ((size_t) RSA_size(rsa));

    for (i = 0; i < 1024 * 1024 * (1024 / 245); i++)
    {
    RSA_public_encrypt(strlen((char*) plainText), plainText, cipherText, rsa, RSA_PKCS1_PADDING);
    }

    free(cipherText);
    RSA_free(rsa);

    return 0;
    }

    위의 같잖은 프로그램들[각주:1]을 이용해 1GByte에 해당되는 데이터를 각각 5번씩 암호화 하고 가장 느린 것 둘을 제외한 나머지의 평균을 비교해 보도록 하겠다. 여러번 반복해서 평균을 비교하는 이유는, 인코딩 하는 중간에 태스크 전환이 일어난다던가 하는 등의 프로그램 외적인 문제에 의한 로스 타임이 길어지거나 짧아질 수 있기 때문이다.

    그런데,공개키 방식의 암호화 작업은 너무 느리기 때문에 키의 크기보다 큰 데이터를 암호화해 주지 않는다. 그렇기 때문에 2048비트 크기의 키를 이용해서는 256 바이트 까지만 암호화가 가능하다. 그러나, 암/복호화 이외의 팩터 때문에 그 전부를 암/복호화 할 수 는 없다. 2048 비트 크기의 키를 사용하는 경우 암호화 할수 있는 데이터의 크기는 최대 245 바이트이다. 그래서, RSA 암호화 샘플에서는 반복횟수를 1024 * 1024 회가 아니라, 1024 * 1024 * (1024 / 245) 번을 반복한다.

    아래의 실행결과는 2.5GHz 인텔 i5 쿼드코어, 8G 메인메모리, Windows7 64비트 환경에서 실행한 결과이다. 잡다한 프로그램들이 상당히 많이 깔려있는 컴퓨터 인지라 다른 프로그램의 영향을 받았을 확률이 높다. 사실 쿼드코어 환경이라고 해도, 암호화과정 자체가 병렬처리 될 수 없기 때문에 코어 하나만 사용했다고 봐도 무방하다.

     real     0m3.869s
     user    0m3.837s
     sys     0m0.000s

     real     0m3.822s
     user    0m3.806s
     sys     0m0.015s

     real     0m3.838s
     user    0m3.790s
     sys     0m0.015s

     real     0m3.822s
     user    0m3.806s
     sys     0m0.015s

     real     0m3.822s
     user    0m3.806s
     sys     0m0.000s

    AESSpeedTest 실행결과

    AES의 평균 처리 속도는 약 3.801s이고, 초당 처리 바이트 수는 282,489,299 바이트이다. 128비트 AES를 사용하였기 때문에, 초당 17,655,581번의 AES 연산이 수행되는 셈이 된다.

     real     3m32.800s
     user    3m32.769s
     sys     0m0.000s

     real     3m32.613s
     user    3m32.566s
     sys     0m0.015s

     real     3m32.738s
     user    3m32.644s
     sys     0m0.015s

     real     3m32.831s
     user    3m32.722s
     sys     0m0.000s

     real     3m32.987s
     user    3m32.878s
     sys     0m0.046s

    RSASpeedTest 실행결과

    RSA의 평균 처리 속도는 212.644s 이고, 초당 처리 바이트 수는 5,049,481 바이트 이다. 2048비트 RSA를 사용하였으니, 초당 20,610 번의 RSA 연산을 수행한 셈이 된다.

    사실, 실험결과를 가지고 계산해 볼 필요도 없을 정도로 차이가 극명하다. 블록암호의 경우에는 16바이트 정도의 배열과 비트열을 가지고 깔짝대는 정도의 연산 뿐 이지만, 공개키 방식의 암호는 256바이트를 대상으로 덧셈, 곱셈, 정수 나눗셈을 해야 한다. 그러니 실험을 해볼 필요도 없이 AES의 속도 - 블록 암호의 속도가 빠를 수 밖에 없다.

    정녕, 단매에 죽고 싶은게냐? 대체 쓸데없는 실험은 왜한 것이냐!

    사실, 16바이트만 암호화 하는 경우는 있을 수 없는 데다가, ECB 방식은 쓰지 말라고 했으니, 위의 실험 결과는 사실 실제로 프로그램을 작성했을 때 어느정도의 시간을 필요로 하는지를 예측하는 데에는 사용할 수 없는 데이터 이다. 그래서, 여기에서 설명하고자 하는 것과는 직접적인 관계는 없지만, 실제 파일을 대상으로 암호화 작업을 했을 때 어느정도 시간이 소요되는지 간단한 실험을 해보도록 하겠다.

    1,761,397,888 바이트 짜리 파일을 다음의 커멘드를 이용하여 암호화 해 보니 다음과 같은 결과를 얻었다.

    time openssl aes-128-cbc -in 1761397888_byte_long.bin -K 000102030405060708090a0b0c0d0e0f -iv 000102030405060708090a0b0c0d0e0f -e -out encrypted_file.enc

    real 0m55.238s
    user 0m11.480s
    sys
    0m3.023s

    리얼이 상당히 길고, 유저 타임이 짧은 걸 봐서는 대부분의 시간을 파일 액세스 하기위해서 대기하는데 보냈다는 것을 알 수 있다. 어찌 되었든 우리에게 필요한 데이터는 두번째의 유저타임 뿐이다. 왜? 대부분의 경우 메모리내 데이터를 암호화 해서 보내지 덩치큰 파일을 암호화 해서 보내거나 하지는 않을 것 이기 때문이다. 대략 초당 153,431,871 바이트, 9,589,491개의 16바이트 블록을 암호화 할 수 있다는 것을 알 수 있다. 파일 액세스나 많은 다른 요소들 때문에 속도가 절반 이하로 줄어들기는 했지만, 실제 환경에서도 다른 작업과 병행해서 작업을 할 것 이므로 비슷한 정도로 암호화/복호화 할 수 있을 것 이다.

    AES의 경우, 속도를 희생한다면 대략 1KB ~ 4KB 사이에서 구현이 가능하다. 일반적인 방식을 사용여 효율을 우선시 한 경우 40KB 정도의 코드 및 데이터로 암복호화 모듈을 만들 수 있다. 그러므로, 효율이나 안전성을 우선으로 생각한다면, 공개키 방식 보다는 블록 암호를 사용하는 편이 더 나은 선택 이라는 것을 알 수 있을 것 이다.

  2. 둘을 적당히 섞은 PGP(Pretty Good Privacy) 방식의 암호체계

    앞에서 말 했듯이 비 대칭키 방식과 대칭키 방식은 각각의 특징을 가지고 있고, 서로 다른 상황에서 사용하기 위해서 만들어진 것이다. 그러나, 두 방식의 특징을 섞어서 장점만 취하고 싶은 것이 인지상정! 그래서 만들어진 것이 PGP 방식의 암호화 방식이다. PGP를 한마디로 설명하자면 '본문을 대칭키로 암호화 하고 그 대칭키를 공개키로 암호화 하여 전달하는 방식'이다.

    통신을 시도하게 되면 

    1. 통신을 위해 서버와 클라이언트가 각각 공개키/개인키를 생성한다.

    2. 상대방에게 생성된 공개키를 전송한다.

    3. 세션키로 사용할 대칭 키를 하나 생성한다. 이 세션 키는 말 그대로 통신을 수행하는 동안에 사용되는 대칭키 이다. 대칭키의 특성상 같은 키를 이용해서 많은 데이터를 암호화 하면 할 수록 깨기 쉬워지기 때문에, 말 그대로 한 세션 동안에만 사용되는 키는 상대적으로 공격에 강할 수 밖에 없다.

    4. 만들어진 세션키를 이용하여 전송 하고자 하는 데이터를 암호화 한다.

    5. 만들어진 세션키를 상대편의 공개키를 이용해 암호화 한다.

    6. 암호화된 세션키와 그 세션키로 암호화 된 데이터를 묶는다.

    7. 이렇게 만들어진 데이터를 전달해 주면 된다.!!!

    이렇게 묶어줌으로서 공개키 방식의 안전한 키 전달 방식과, 블록 암호 방식의 강력한 암호화 특성을 모두 취할 수 있는 아주 좋은 방식이다. 사실, 제목에는 어정쩡 하다는 자극적인 표현을 달았지만 실제로 기존에 나와있는 암호화 통신 방식중에 이 이상으로 좋은 방식이 있기 힘든 아주 좋은 방식이다. PGP는 어찌 되었든 '상용'이며, GNU에서 GNU Privacy Guard (GnuPG 혹은 GPG)를 PGP 5.x 를 대체하는 공개 소프트웨어로 제공하고 있다.

게임의 패킷을 전달 하기 위해 그냥 PGP나 GPG를 사용하는 것도 나쁘지 않은 선택이 되겠지만, 다목적의 통신 패킷을 사용하는 것은 효율성 면에서 그다지 좋은 선택이 아닐 수 도 있다. 그렇기 때문에 작업중인 게임의 특성에 맞추어 암호화 통신을 위한 프로토콜을 만들어 보는 것도 좋은 것 이다. 다음 번 부터는 프로토콜을 디자인 하는 작업을 해 보겠다. 프로토콜을 디자인 하는데 필요한 바탕 지식과, 프로토콜을 공격하는 공격 기법을 설명하고, 그 공격을 어떻게 회피할 지에 대해 설명을 하고 함께 프로토콜을 디자인 해 보도록 하겠다.

*     *     *

역시 생각만큼 생각을 풀어내는 것이 쉽지는 않네요, 어디 까지 상세하게 써야 하는 건지도 확실하지가 않은지라 할수록 어렵네요. 계속 써야 하는지도 막막 고민이 되네요... 뭐, 보든 말든 어찌 되었든 쓸겁니다.

  1. 이전의 포스팅에서 하지말라고 했던 모든 짓을 했다고 해서 뭐라 하지는 말아 주세욥! 말 그대로 실험을 위해서 작성한 코드이고, 모든 사람들이 쉽게 알아보게 하도록 하기 위해 그렇게 했습니다. [본문으로]
반응형
,