시작하며
이제부터 본격적으로 크립토 라이브러리의 구현에 대해 다뤄보고자 한다. 암복호화에 관련된 함수들은 openssl을 이용하여 구현하였고 블록암호운용모드에 대한 코드들을 직접 작성하였다. 이 라이브러리에선 CBC와 CTR, 두개의 모드를 지원하였고 CTR은 스트림 암호도 가능한 것으로 알고있으나 여기선 16바이트 블록단위의 암호화 용으로만 사용하였다. 이번 글에선 이 두개의 운용모드를 어떻게 함수로써 구현하였는지 설명하겠다.
1. Concept
이 라이브러리에서 CBC와 CTR을 구현하기 위해 openSSL에서 제공하는 ECB함수를 사용한다. ECB에 패딩을 붙이지 않고 무조건 16바이트의 데이터만 파라미터로 넘겨준다면 정확하게 한 블록을 암호화하는 함수로써 ECB를 사용할 수 있을 것이다. 다음은 간단하게 표현한 ECB 함수이다.
위 그림은 대략적인 ECB 모드의 그림이다. 각 블록별로 개별적으로 암호화 처리를 한다. 각 블록단위의 평문을 인풋으로 받아 아웃풋으로 그 블록에 암호문을 만들어낸다. 이 떄 이 라이브러리에서 암호화 알고리즘은 AES128로 설정하였다. 그렇다면 16바이트 단위로 암호화하는 ECB가 있다고 가정할 때, 이 ECB에 패딩옵션을 넣지 않고 무조건 16바이트만 인풋으로 넣어준다면, 16바이트의 암호문을 아웃풋으로 내는 한블록짜리 암호화 함수로써 ECB를 사용할 수 있을 것이다. 즉 본인이 구현한 CBC, CTR 함수는 내부에서 이 ECB 함수를 암호화 함수로써 사용한다. 이해를 돕기 위해 CBC를 예시로 들어보겠다.
이전 암호문을 현재 평문가 xor하여 암호화를 진행하는 CBC함수를 간단하게 그린 것이다. 이 때, 그림에서 암호화를 진행하는 부분, 저 부분에서 우리는 ECB함수를 암호화 함수로써 사용한다는 것이다. 그 외 복호화 또 CTR 모드 같은 경우 궁금할 경우 인터넷을 찾아보길 바란다. 아무튼 ECB함수를 이용해 CBC와 CTR을 구현한다는 것을 이해했다고 믿고 다음으로 넘어가겠다.
2. 구현
먼저 CBC부터 살펴보자
//ecb함수를 이용해 cbc방식 암호화를 구현
I_LOCAL void enc_ecb_to_cbc(int p_cipher_id, //enc cbc_using_ecb
AES_KEY* p_key,
uint8_t* p_input,
uint32_t p_inputlength,
uint8_t* p_output,
uint32_t* p_outputlength,
uint8_t* p_iv,
uint32_t p_ivlength) {
uint8_t block[16]; //plain_text xor pre_enc_data
uint32_t blocklength = 16; //block 길이
uint32_t lastBlockLength = p_inputlength % blocklength;//블록단위로 나눈 후 남은 데이터 길이 ex) blocklength : 16, input : 33 -> lastBlockLength : 1
uint8_t ecb_output[16] = { 0x00, };
uint32_t dataIndex = 0; //block의 길이만큼 증가하는 데이터의 인덱스
for (int i = 0; i < p_ivlength; i++)
block[i] = p_input[i] ^ p_iv[i];
//마지막 블록 전
while ((int)dataIndex <= ((int)p_inputlength - (int)blocklength)) {
if (dataIndex == 0)//first enc...Ek(p ^ iv)
AES_ecb_encrypt(block, ecb_output, p_key, AES_ENCRYPT);
else {//after first enc.... Ek(P(i) ^ C(i-1))
for (int i = dataIndex; i < blocklength + dataIndex; i++)//plain_text xor pre_enc_data
block[i % blocklength] = ecb_output[i % blocklength] ^ p_input[i]; //i%blocklength = 0~15
AES_ecb_encrypt(block, ecb_output, p_key, AES_ENCRYPT);
}
for (int i = dataIndex; i < blocklength + dataIndex; i++)//put ecb_enc_data to output
p_output[i] = ecb_output[i % blocklength];
*p_outputlength += blocklength;
dataIndex += blocklength; // 블록단위로 인덱스 갱신
}
}
라인 넘버가 없어서 굉장히 설명하기 힘든데 간단하게 말해서 이 enc_ecb_to_cbc 함수는 ecb를 이용해 cbc를 구현한 함수이다. 이 함수는 매개변수 p_output에 결과를 저장한다. 처음에 p_input과 p_iv(cbc는 처음 이전블록이 없기에 초기벡터 iv와 평문을 xor한다.)를 xor 하여 block에 담고 이를 AES_ecb_encrypt를 사용해 암호화 한 후 이를 ecb_output와 p_output에 담는다. 이후부턴 iv가 아닌 이전 암호문을 담고있는 ecb_output과 p_input을 xor하며 동일하게 AES_ecb_encrypt를 이용해 암호화한다. 이 함수는 앞에 I_LOCAL이라는 키워드가 있는데 이 함수가 외부 노출 함수가 아니기 때문이다. 이 함수는 다음 글에서 다룰 i_enc라는 함수 내부에서만 동작하며, 사용자는 이 함수를 직접 호출하는 것이 아니라 무조건 i_enc를 호출해야하기 때문에 extern이 아닌 로컬 함수라는 것을 의미하는 키워드이다. 실제로 원래라면 마지막에 패딩을 붙이고 암호화를 진행해야 하지만 이 함수에서는 패딩을 붙이는 기능이 없다. 왜냐하면 이 함수를 돌고 난 후 i_enc에서 패딩처리를 하기 때문이다. 이렇게 이 함수는 자체에서는 패딩을 처리를 하지 않는데 이를 사용자가 그냥 쓰게 한다면 분명 문제가 생길 것이다. 즉 이는 내부에서만 쓰고 밖에서 쓸 이유가 없기 때문에 사용자에게 감춰야할 이유가 충분하다. 이 프로젝트를 진행하면서 공개할 코드, 그렇지 않은 코드를 구별하는 법을 확실하게 알 수 있었다. 복호화 함수가 궁금하다면 맨 밑에 깃 링크에서 전체 코드를 볼 수 있으니 참고하기 바란다.
넘어가서 다음은 CTR모드이다 CTR 모드에서 가장 관건은 카운터 값을 증가시키는 것이다. 코드를 보자.
I_LOCAL void i_inc_counter(uint8_t* counter, uint32_t counterlength){
//ctr is big-endian
//openssl에서 16바이트 counter를 uint32(4바이트)단위로 4개씩 끊어 증가 시키기 때문에
for(int i=counterlength-sizeof(uint32_t);i>=0;i-=sizeof(uint32_t)){
uint32_t value = (*((uint32_t*)(counter+i)))++;
uint32_t carry = !value;
if(carry == 0) return;
}
}
이 함수는 CTR모드에서 counter값을 증가시키기 위한 함수이다. 카운터가 총 16바이트인데, 이를 uint32의 사이즈 4바이트 단위로 나눠서 카운터를 증가시킨다. 즉 인덱스 0~3, 4~7, 8~11, 12~15 씩 끊는다. 이때, 리틀엔디안이라면 값을 분명 0~3범위를 먼저 증가시켜야겠지만, 실제 openssl에서 확인해본 결과, 맨 뒤 12~15범위에서 먼저 값을 증가시키는 것을 알게 되었다.
반복문에서 i가 counterlength-sizeof(uint32_t) 로 초기화 되는데, 이를 계산하면 12이다. 카운터모드의 표준이 빅엔디안이기 때문에 맨 마지막 구간부터 값을 올리는 것이다. 값을 증가시키며 캐리가 발생하면 다음 구간으로, 즉 4바이트를 뺀 곳으로 이동하여 값을 증가시키고, 캐리가 발생하지 않으면 바로 리턴을 한다. 4바이트씩 끊은 이유는 결국 1씩 증가시키면서 uint32 범위를 벗어나면, 즉 4바이트가 꽉 찬다면, 캐리가 발생하는 것이기 때문이다. 이 때 4바이트가 맥시멈인 상태에서 값이 증가하면 해당 4바이트는 0이 될 것이며, 그렇게 되면 carry는 반대로 1이 되어 함수가 종료되지 않고 반복을 한 번 더 돌게 되는 원리이다.
여러 잡기술들이 섞여있는데 조금만 생각하면 코드가 금방 이해가 될 것이다. 이제 카운터값을 증가시키는 함수를 만들었기 때문에 CTR 함수는 금방 구현할 수 있다.
I_LOCAL void enc_ctr_mode(int p_cipher_id,
AES_KEY* p_key,
uint8_t* p_input,
uint32_t p_inputlength,
uint8_t* p_output,
uint32_t* p_outputlength,
uint8_t* p_counter,
uint32_t p_counterlength) {
uint8_t block[16];
uint32_t blocklength = 16;
uint8_t ecb_output[16] = { 0x00, };
uint32_t dataIndex = 0; //block의 길이만큼 증가하는 데이터의 인덱스
uint32_t blockNum = p_inputlength / blocklength;
//마지막 블록 전까지
for (int i = 0; i < blockNum; i++) {
AES_ecb_encrypt(p_counter, ecb_output, p_key, AES_ENCRYPT);
i_inc_counter(p_counter, p_counterlength);
for (int j = dataIndex; j < blocklength + dataIndex; j++)
p_output[j] = ecb_output[j % blocklength] ^ p_input[j];
*p_outputlength += blocklength;
dataIndex += blocklength;
}
}
CTR모드가 정확히 어떻게 동작하는지 궁금하다면 검색을 통해 확인해보길 바란다. (그림그리기가 귀찮아서 위에 cbc까지만 그렸슴다ㅠ)
CTR은 평문부터가 아니라 카운터 값을 먼저 암호화하고 이 암호화한 결과를 평문과 xor한다. 코드도 보면 먼저 p_counter를 AES_ecb_encrypt로 암호화 한 후 이 결과를 p_input과 최종적으로 하여 p_output에 넣는 것을 볼 수 있다. 이 때 암호화 함수를 호출하여 카운터 값을 암호화 했다면 바로 밑에 i_inc_counter 함수를 호출해 카운터값을 증가시키는 것을 확인할 수 있다.
위의 두 함수도 다른 함수 내부에서만 동작하고 사용자가 직접 호출하지 못하는 함수이므로 I_LOCAL 키워드가 붙은 것을 알 수 있다. 이 다음글에서는 이들 함수를 사용하는 i_enc와 i_dec 함수에 대한 글을 작성하겠다.
github : https://github.com/0xGh-st/I_CRYPTO.git
GitHub - 0xGh-st/I_CRYPTO
Contribute to 0xGh-st/I_CRYPTO development by creating an account on GitHub.
github.com
'C > Crypto' 카테고리의 다른 글
Crypto Project : 1-5. 암복호화 init, update, final 구현 (0) | 2023.04.16 |
---|---|
Crypto Project : 1-4. C언어에서 멤버변수 접근을 막는 방법과 init, update, final의 개념 (0) | 2023.04.15 |
Crypto Project : 1-3. 암호화, 복호화 함수(enc, dec) (1) | 2023.04.14 |
Crypto Project : 1-1. i_crypto library - 구조 및 문서화 예시 (0) | 2023.04.11 |
Crypto Project : Prologue (0) | 2023.04.10 |