Free Lines Arrow
본문 바로가기
Language/C++

[C++] Crypto++ 을 이용한 string 암호화 구현

by skahn1215 2021. 7. 16.
728x90
반응형

Crypto++ 을 이용한 string 암호화

 

시작이유

회사 에서 암호화를 구현해야 될 일이 생겼다.
DB 의 중요한 내용을 암호화 해야 되기 때문이다.

예제는 제일 마지막에 있으니 쓰시고 싶으신 분들은
아래에서 복붙 하시면 됩니다.

단 출처를 남겨주세요

설치

리눅스

sudo apt-get install libcrypto++-dev libcrypto++-doc libcrypto++-utils


윈도우
https://www.cryptopp.com/#download

 

Crypto++ Library 8.5 | Free C++ Class Library of Cryptographic Schemes

key agreement schemes Diffie-Hellman (DH), Unified Diffie-Hellman (DH2), Menezes-Qu-Vanstone (MQV), Hashed MQV (HMQV), Fully Hashed MQV (FHMQV), LUCDIF, XTR-DH

www.cryptopp.com

 

분석

1. DB 파일 자체를 암복호화 한다?
- 위험성이 좀 크다고 생각한다.
- 실제로 몇번 테스트 하다가 뻑이 나는 경우도 생겼고
- 동일한 키, 값이 중복 저장 되는 문제가 발생했다.

2. string 만 암호화 해서 넣고 빼기
- 제일 간편해 보인다.

구현전 사전 지식

구현하기전 복붙 하는게 아니라 API 분석 및
암호화 관련 해서 어떤 모드를 사용할 것인지 분석할 필요가 있었다.

블록 암호 운용 방식

블록 암호운영 방식에는 어떤게 있는지 알아 보자.

암호학에서 블록 암호 운용 방식(영어: block cipher modes of operation)은 하나의 키 아래에서 블록 암호를 반복적으로 안전하게 이용하게 하는 절차를 말한다.

블록 암호는 특정한 길이의 블록 단위로 동작하기 때문에, 가변 길이 데이터를 암호화하기 위해서는 먼저 이들을 단위 블록들로 나누어야 하며, 그리고 그 블록들을 어떻게 암호화할지 정해야 하는데, 이때 블록들의 암호화 방식을 운용 방식이라 부른다


초기화 벡터

운용 방식마다 초기화 벡터를 사용하는 방법이 다르며 초기화 벡터에서 요구되는 성질도 조금씩 다를 수 있지만, 같은 초기화 벡터가 반복되어 사용되어서는 안 된다는 성질을 공통적으로 가진다. 이것은 초기화 벡터가 같은 경우 비슷한 두 개의 평문을 암호화했을 때 앞부분의 블록들이 서로 같게 되는 등 보안 문제가 발생하기 때문이다.
CTR 등의 일부 운용 방식에서는 초기화 벡터라는 용어 대신 nonce(number used once)라는 용어를 사용한다. 이 표현은 보통 초기화 벡터가 매번 달라야 하는 것 외에 별다른 요구 조건이 없을 경우 사용한다


패딩

데이터를 암호화 할때 항상 블럭의 크기가 일정하지 않으므로 마지막 부족한 부분을 채워 주는 방법을 말한다.

728x90

운용방식 종류

  • ECB: 전자코드북
  • CBC: 암호 블록체인 방식
  • CFB: 암호 피드백
  • OFB: 출력 피드백
  • CTR: 카운터

이외에도 여러가지 방식이 있는데 이정도만 보려고 한다.

ECB

전자 코드북(electronic codebook, ECB)은 운용 방식 중 가장 간단한 구조를 가지며, 암호화하려는 메시지를 여러 블록으로 나누어 각각 암호화하는 방식으로 되어 있다.

전자 코드북은 모든 블록이 같은 암호화 키를 사용하기 때문에 보안에 취약하다. 만약 암호화 메시지를 여러 부분으로 나누었을 때 두 블록이 같은 값을 가진다면, 암호화한 결과 역시 같다. 이것은 공격자가 비슷한 메시지를 반복적으로 암호화하는 반복공격 에도 취약한 성질을 가진다.

CBC

암호 블록 체인 (cipher-block chaining, CBC) 방식은 1976년 IBM에 의해 개발되었다.[4] 각 블록은 암호화되기 전에 이전 블록의 암호화 결과와 XOR되며, 첫 블록의 경우에는 초기화 벡터가 사용된다. 초기화 벡터가 같은 경우 출력 결과가 항상 같기 때문에, 매 암호화마다 다른 초기화 벡터를 사용해야 한다.

CBC 방식은 현재 널리 사용되는 운용 방식 중 하나이다. CBC는 암호화 입력 값이 이전 결과에 의존하기 때문에 병렬화가 불가능하지만, 복호화의 경우 각 블록을 복호화한 다음 이전 암호화 블록과 XOR하여 복구할 수 있기 때문에 병렬화가 가능하다.


CFB

암호 피드백(cipher feedback, CFB) 방식은 CBC의 변형으로, 블록 암호를 자기 동시 스트림 암호로 변환한다. CFB의 동작 방식은 CBC와 비슷하며, 특히 CFB 암호 해제 방식은 CBC 암호화의 역순과 거의 비슷하다



OFB

출력 피드백(output feedback, OFB)은 블록 암호를 동기식 스트림 암호로 변환한다.
XOR 명령의 대칭 때문에 암호화와 암호 해제 방식은 완전히 동일하다:

 

CTR

카운터(Counter, CTR) 방식은 블록 암호를 스트림 암호로 바꾸는 구조를 가진다. 카운터 방식에서는 각 블록마다 현재 블록이 몇 번째인지 값을 얻어, 그 숫자와 nonce를 결합하여 블록 암호의 입력으로 사용한다. 그렇게 각 블록 암호에서 연속적인 난수를 얻은 다음 암호화하려는 문자열과 XOR한다.
카운터 모드는 각 블록의 암호화 및 복호화가 이전 블록에 의존하지 않으며, 따라서 병렬적으로 동작하는 것이 가능하다. 혹은 암호화된 문자열에서 원하는 부분만 복호화하는 것도 가능하다.

 

구현 예제

CBC 예제를 사용해서 암복호화를 하고 좀더 다듬어서 실제 회사 코드에 반영을 해본다.

CryptoExample.h

#include <crypto++/hex.h>
using CryptoPP::HexEncoder;
using CryptoPP::HexDecoder;

#include <crypto++/filters.h>
using CryptoPP::StringSink;
using CryptoPP::StringSource;
using CryptoPP::StreamTransformationFilter;

#include <crypto++/aes.h>
using CryptoPP::AES;

#include <crypto++/ccm.h>
using CryptoPP::CBC_Mode;

#include <assert.h>

class CryptoAPI {
private:
    byte *mKey;
    byte *mIv;
    int mKeyLength;
    int mIvLength;

    const string FAIL_GENERATE_KEY_EX_MSG = "fali to generate key";
    const string FAIL_ENCRYPT_FILE_EX_MSG = "fail to encrypt file";
    const string FAIL_DECRYPT_FILE_EX_MSG = "fail to decrypt file";
    const string FAIL_OPEN_INPUT_FILE_EX_MSG  = "fail to open input file";
    const string FAIL_OPEN_OUTPUT_FILE_EX_MSG = "fali to open output file";
    const string FAIL_ALLOCATION_BUFFER_EX_MSG = "fail to allocate buffer";
    const string INVALID_KEY_EX_MSG = "The key is invalid";


public:
    enum class SECURITY_EXCEPTION {
        NO_EXCEPTION_EX,
        FAIL_GENERATE_KEY_EX,
        FAIL_ENCRYPT_FILE_EX,
        FAIL_DECRYPT_FILE_EX,
        FAIL_OPEN_INPUT_FILE_EX,
        FAIL_OPEN_OUTPUT_FILE_EX,
        FAIL_ALLOCATION_BUFFER_EX,
        INVALID_KEY_EX
    };

    CryptoAPI();
    ~CryptoAPI();
    bool generateKey(string keyPath, string ivPath);
    bool encryptString(const string &inputText, string &outPutText);
    bool decryptString(const string &inputText, string &outPutText);
    bool setKey(string keyPath, string ivPath);

    int printExceptionMessage(SECURITY_EXCEPTION e);
};

 

반응형

 

CryptoExample.cpp

#include "CryptoAPIExample.h"


CryptoAPI::CryptoAPI()
{
    mKeyLength = 0;
    mIvLength = 0;

    mKey = NULL;
    mIv = NULL;

    try {
        mKey = (byte*)malloc(sizeof(byte) * AES::DEFAULT_KEYLENGTH);
        mIv = (byte*)malloc(sizeof(byte) * AES::BLOCKSIZE);
        mKeyLength = AES::DEFAULT_KEYLENGTH;
        mIvLength = AES::BLOCKSIZE;

        if ( (mKey==NULL) || (mIv == NULL) ||
             (mKeyLength == 0) || (mIvLength == 0) )
        {
            throw SECURITY_EXCEPTION::FAIL_ALLOCATION_BUFFER_EX;
        }

    }
    catch(SECURITY_EXCEPTION e) {
        cout<< "Error Code: " << printExceptionMessage(e) << endl;
    }
}

CryptoAPI::~CryptoAPI()
{
    if (mKey != NULL) {
        free(mKey);
    }

    if (mIv != NULL) {
        free(mIv);
    }
}

bool
CryptoAPI::generateKey(string keyPath, string ivPath)
{

    bool ret = false;
    try {
        // 자동으로 생성된 랜덤 시드 풀
        AutoSeededRandomPool prng;

        // 키 생성
        prng.GenerateBlock(mKey, mKeyLength);
        prng.GenerateBlock(mIv, sizeof(mIv));

        // 단순히 key값을 hexa 값으로 출력 해주는 부분
        // Pretty print key
        /*
        string encoded;
        encoded.clear();
        StringSource(mKey, mKeyLength, true,
            new HexEncoder(new StringSink(encoded)) // HexEncoder
        ); // StringSource
        cout << "key: " << encoded << endl;
        */

        // 초기화 벡터 값을 hexa로 출력해 주는 부분
        // Pretty print iv
        /*
        encoded.clear();
        StringSource(mIv, sizeof(mIv), true,
            new HexEncoder(new StringSink(encoded)) // HexEncoder
        ); // StringSource
        cout << "iv: " << encoded << endl;
        */

        // key 를 파일로 저장
        // 후에 동일 한 키로 암복호화 할 때 필요
        std::ofstream keyFileSteam;
        keyFileSteam.open(keyPath, std::ios::out | std::ios::binary);
        if (keyFileSteam.is_open()) {
            keyFileSteam.write((char*)mKey, mKeyLength);
            keyFileSteam.close();
        } else {
            throw SECURITY_EXCEPTION::FAIL_OPEN_INPUT_FILE_EX;
        }
        

        // iv 초기화 벡터를 파일로 저장
        // 후에 동일 한 키로 암복호화 할 때 필요
        std::ofstream ivFileSteam;
        ivFileSteam.open(ivPath, std::ios::out | std::ios::binary);
        if (ivFileSteam.is_open()) {
            ivFileSteam.write((char*)mIv, mIvLength);
            ivFileSteam.close();
        } else {
            if (keyFileSteam.is_open()) {
                keyFileSteam.close();
            }
            throw SECURITY_EXCEPTION::FAIL_OPEN_OUTPUT_FILE_EX;
        }

        if (mKey == NULL || mIv == NULL) {
            throw SECURITY_EXCEPTION::FAIL_GENERATE_KEY_EX;
        }

        ret = true;
    }
    catch (SECURITY_EXCEPTION e)
    {
        cout<< "Error Code: " << printExceptionMessage(e) << endl;
    }
    catch(const CryptoPP::Exception& e)
    {
        cerr << e.what() << endl;
    }

    return ret;
}

// 기존에 파일로 저장한 키를 이용할 경우
bool
CryptoAPI::setKey(string keyPath, string ivPath)
{

    bool ret = false;
    try {
        if (mKey == NULL || mIv == NULL) {
            throw SECURITY_EXCEPTION::FAIL_GENERATE_KEY_EX;
        }

        int keyFileSize = 0;
        ifstream inputKeyfile;
        inputKeyfile.open(keyPath, std::ios::binary | std::ios::in);
        inputKeyfile.seekg(0, ios_base::end);//get binary file length
        keyFileSize = inputKeyfile.tellg();//get binary file length
        inputKeyfile.seekg(0, ios::beg);//get binary file length

        char *keybuffer = new char[keyFileSize];

        inputKeyfile.read(keybuffer, keyFileSize);
        inputKeyfile.close();


        memcpy ( mKey, keybuffer, keyFileSize );
        //mKey = reinterpret_cast<byte*>(keybuffer);
        delete keybuffer;

        if (keyFileSize != AES::DEFAULT_KEYLENGTH) {
            throw SECURITY_EXCEPTION::INVALID_KEY_EX;
        }

        int ivFileSize = 0;
        ifstream inputIvfile;
        inputIvfile.open(ivPath, std::ios::binary | std::ios::in);
        inputIvfile.seekg(0, ios_base::end);//get binary file length
        ivFileSize = inputIvfile.tellg();//get binary file length
        inputIvfile.seekg(0, ios::beg);//get binary file length

        char *ivbuffer = new char[ivFileSize];

        inputIvfile.read(ivbuffer, ivFileSize);
        inputIvfile.close();

        memcpy ( mIv, ivbuffer, ivFileSize );
        //mIv = reinterpret_cast<byte*>(ivbuffer);
        delete ivbuffer;

        if (ivFileSize != AES::BLOCKSIZE) {
            cout<< ivFileSize <<endl;
            throw SECURITY_EXCEPTION::INVALID_KEY_EX;
        }
        ret = true;
    }
    catch (SECURITY_EXCEPTION e)
    {
        cout<< "Error Code: " << printExceptionMessage(e) << endl;
    }
    catch(const CryptoPP::Exception& e)
    {
        cerr << e.what() << endl;
    }

    return ret;
}

// 암호화
bool
CryptoAPI::encryptString(const string &inputText, string &outPutText)
{

    bool ret = false;

    try
    {
        if (mKey == NULL || mIv == NULL) {
            throw SECURITY_EXCEPTION::INVALID_KEY_EX;
        }
        
         
        // CBC 방식으로 암호화 
        CBC_Mode< AES >::Encryption e;
        
        // 암호화에 필요한 초기 벡터 값과 키 를 셋팅한다.
        e.SetKeyWithIV(mKey, mKeyLength, mIv);

        // string source를 이용하여 StreamTransformationFilter 타입에 맞춰 변환
        // 즉 암호화 하는 구간.
        
        // The StreamTransformationFilter removes
        //  padding as required.
        StringSource s( inputText, true,
            new StreamTransformationFilter(e,
            new StringSink(outPutText)) // StreamTransformationFilter
                ); // StringSource

#if 0
                StreamTransformationFilter filter(e);
                filter.Put((const byte*)plain.data(), plain.size());
                filter.MessageEnd();

                const size_t ret = filter.MaxRetrievable();
                cipher.resize(ret);
                filter.Get((byte*)cipher.data(), cipher.size());
#endif
            ret = true;
        }
        catch (SECURITY_EXCEPTION e) {
            cout<< "Error Code: " << printExceptionMessage(e) << endl;
        }
        catch(const CryptoPP::Exception& e)
        {
                cerr << e.what() << endl;
                return 0;
        }

    return ret;
}

bool
CryptoAPI::decryptString(const string &inputText, string &outPutText)
{
    bool ret = false;
    try
    {
        if (mKey == NULL || mIv == NULL)
        {
            throw SECURITY_EXCEPTION::INVALID_KEY_EX;
        }

        CBC_Mode< AES >::Decryption d;
        d.SetKeyWithIV(mKey, mKeyLength, mIv);

        // The StreamTransformationFilter removes
        //  padding as required.
        StringSource s(inputText, true,
                        new StreamTransformationFilter(d,
                        new StringSink(outPutText)
                        ) // StreamTransformationFilter
                       ); // StringSource

#if 0
        StreamTransformationFilter filter(d);
        filter.Put((const byte*)cipher.data(), cipher.size());
        filter.MessageEnd();

        const size_t ret = filter.MaxRetrievable();
        recovered.resize(ret);
        filter.Get((byte*)recovered.data(), recovered.size());
#endif
            ret = true;
        }
        catch (SECURITY_EXCEPTION e) {
            cout<< "Error Code: " << printExceptionMessage(e) << endl;
        }
        catch(const CryptoPP::Exception& e)
        {
            cerr << e.what() << endl;
        }

        return ret;
}

int
CryptoAPI::printExceptionMessage(SECURITY_EXCEPTION e)
{
    if (e == SECURITY_EXCEPTION::FAIL_GENERATE_KEY_EX) {
        cout << FAIL_GENERATE_KEY_EX_MSG << endl;
    } else if (e == SECURITY_EXCEPTION::FAIL_ENCRYPT_FILE_EX) {
        cout << FAIL_ENCRYPT_FILE_EX_MSG << endl;
    } else if (e == SECURITY_EXCEPTION::FAIL_DECRYPT_FILE_EX) {
        cout << FAIL_DECRYPT_FILE_EX_MSG << endl;
    } else if (e == SECURITY_EXCEPTION::FAIL_OPEN_INPUT_FILE_EX ) {
        cout << FAIL_OPEN_INPUT_FILE_EX_MSG << endl;
    } else if (e == SECURITY_EXCEPTION::FAIL_OPEN_OUTPUT_FILE_EX) {
        cout << FAIL_OPEN_OUTPUT_FILE_EX_MSG << endl;
    } else if (e == SECURITY_EXCEPTION::FAIL_ALLOCATION_BUFFER_EX) {
        cout << FAIL_ALLOCATION_BUFFER_EX_MSG << endl;
    } else if (e == SECURITY_EXCEPTION::INVALID_KEY_EX) {
        cout << INVALID_KEY_EX_MSG << endl;
    }

    return (int)e;
}

 

CryptoTest.cpp

#include<iostream>
#include"CryptoAPIExample.h"
using namespace std;

int main(int argc, char* argv[])
{   
    std::string plainText = "TESTSTRING:123123123";
    std::string encText = "";
    std::string decText = "";

    // encrypt test
    CryptoAPI *crypto = new CryptoAPI();
    crypto->generateKey("/home/skahn/work/key","/home/skahn/work/iv");
    crypto->encryptString(plainText, encText);
    crypto->decryptString(encText, decText);

    cout << "NORMAL TEXT: " << plainText << endl;
    cout << "ENCRYPT TEXT: " << encText << endl;
    cout << "DECRYPT TEXT: " << decText << endl;


    // ket set test
    CryptoAPI *crypto2 = new CryptoAPI();
    std::string decText2;
    crypto2->setKey("/home/skahn/work/key","/home/skahn/work/iv");
    crypto2->decryptString(encText, decText2);
    cout << "SETKEY: DECRYPT TEXT: " << decText2 << endl;

    // decrypt fail test
    std::string decryptFail = "";
    crypto2->generateKey("/home/skahn/work/key2","/home/skahn/work/iv2");
    crypto2->decryptString(encText, decryptFail);
    cout << "SETIDFFKEY: DECRYPT TEXT: " << decryptFail << endl;
}

 

Makefile

cryptotest.out: cryptoAPITest.o cryptoapi.o
        g++ -o cryptotest.out cryptoAPITest.o cryptoapi.o  -lcryptopp -lpthread

cryptoAPITest.o: CryptoAPIExample.h CryptoAPITest.cpp
        g++ -c -o cryptoAPITest.o CryptoAPITest.cpp -lcryptopp -lpthread

cryptoapi.o: CryptoAPIExample.h CryptoAPIExample.cpp
        g++ -c -o cryptoapi.o CryptoAPIExample.cpp -lcryptopp -lpthread

clean:
        rm -f *.o

 

결과

// 일반 텍스트
NORMAL TEXT: TESTSTRING:123123123

// 암호화 성공
ENCRYPT TEXT: ���ӄE�s6��E�^���a2hݪ�yi����

// 복호화 성공
DECRYPT TEXT: TESTSTRING:123123123

// 키 파일을 가져와서 복호화
SETKEY: DECRYPT TEXT: TESTSTRING:123123123

// 암호화 키와 복호화 키가 다를 경우 에러
StreamTransformationFilter: invalid PKCS #7 block padding found
SETIDFFKEY: DECRYPT TEXT: \�f��s	�=t�Yv@


일단 기본적인 테스트를 통해 string 암복호화가 잘 되는 것을 확인 했다.

단 출처를 남겨주세요

728x90
반응형

'Language > C++' 카테고리의 다른 글

[C++] string contains  (0) 2021.06.12
[C++] lambda(람다)  (0) 2021.04.06

댓글