728x90

AES 구조



AES(Advanced Ecryption Standard)는 1997년 DES를 대체할 목적으로 NIST 암호 표준 공모를 하여 2001년에 채택된 암호 표준이다.


feistel 구조인 DES와 달리 SPN구조로 되어있어서 복호화시 역연산의 알고리즘을 따로 구현해야 하지만 병렬 처리를 하기 용이하다..


확장성을 고려하여 설계되어서 키 길이는 128/192/256 bit를 가지고 키 길이에 상관없이 블록 길이는 128 bit이며 라운드 수는 10/12/14를 가진다.


대략 적인 순서는


1. 라운드키와 xor한다. (AddRoundKey)


2. 바이트를 치환한다. (SubBytes)


3. 행별로 바이트를 옮긴다. (ShiftRows)


4. 열 별로 바이트를 섞는다. (MixColumns)


5. 라운키와 xor한다. (AddRoundKey)


6. (라운드 수 - 1) 만큼 2~5를 반복한다.


7. 마지막 라운드는 MixColumns를 제외하고 수행한다.


복호화는 각 단계의 역연산을 거꾸로 수행하면 된다.


이제 각 단계를 살펴보자.



aes는 입력 블럭을 위 그림과 같은 행렬로 구성하여 처린한다. 최 상위 한바이트씩 총 16바이트 가 들어가는데 주의할 점은


S00 , S01, S02, ......., S33 순이 아닌 S00, S10, S20, ......, S33순으로 즉 열 우선으로 넣어야 한다.



void AddRoundKey(BYTE state[][4], WORD* rKey) {
	int i, j;
	WORD mask, shift;

	for (i = 0; i < 4; i++) {
		shift = 24;
		mask = 0xff000000;

		for (j = 0; j < 4; j++) {
			state[j][i] = ((rKey[i] & mask) >> shift) ^ state[j][i];
			mask >>= 8;
			shift -= 8;
		}
	}
}


단순하게 state행렬의 각 바이트와 라운드 키 각 바이트와 xor한다.



Subbytes



SubBytes는 위 행렬식을 곱하여 바이트를 치환한다. X0 ~ X7순으로 바이트의 최상위 비트를 위치하여 계산한다.


메모리가 부족한 시스템이라면 직접 계산을 하여 구하지만 보통은 미리 모든 바이트가 계산된 치환표를 사용한다.


SubBytes 치환표



가령 0x37을 치환하면 0xb2로 바뀐다. 복호화시에는 동일하게 역 치환표도 있다.


치환표를 사용하지 않고 계산할 때에는 해당 바이트에 [1 0 1 0 0 0 0 0]을 빼고 위 8x8행렬의 역행렬을 곱해주면 된다.



void SubBytes(BYTE state[][4]) {
	int i, j;

	for (i = 0; i < 4; i++)
		for (j = 0; j < 4; j++)
			state[i][j] = S_box[HIHEX(state[i][j])][LOWHEX(state[i][j])];
}

* HIHEX(), LOWHEX()는 각각 상위4비트 하위4비트를 반환하는 함수.


각각의 바이트에 대하여 구해준다.




ShiftRows



첫행은 움직이지 않고 둘째행은 1번, 셋째행은 2번, 넷째행은 3번 바이트단위로 왼쪽순환쉬프트를 한다.


void ShiftRow(BYTE state[][4]) {
	int i, j;

	for (i = 1; i < 4; i++)
		for (j = 0; j < i; j++)
			CirshiftRows(state[i]);
}

void CirshiftRows(BYTE state[]) {
	BYTE temp = state[0];

	state[0] = state[1];
	state[1] = state[2];
	state[2] = state[3];
	state[3] = temp;
}


코드도 간단하다.



MixColumns 행렬식



MixColumns는 배열의 각 열에 대하여 위 그림과 같은 연산을 수행하는데 기약다항식 x^8 + x^4 + x^3 + x + 1 = 0 상에서 하게 되며 따라서


캐리 발생시 0001 1011을 xor한다.


void MixColumns(BYTE state[][4]) {
	int i, j, k;
	BYTE a[4][4] = { 0x2, 0x3 , 0x1, 0x1,
		0x1, 0x2, 0x3, 0x1,
		0x1, 0x1, 0x2, 0x3,
		0x3, 0x1, 0x1, 0x2 };

	for (i = 0; i < 4; i++) {
		BYTE temp[4] = { 0, };

		for (j = 0; j < 4; j++)
			for (k = 0; k < 4; k++)
				temp[j] ^= x_time(state[k][i], a[j][k]);

		state[0][i] = temp[0];
		state[1][i] = temp[1];
		state[2][i] = temp[2];
		state[3][i] = temp[3];
	}
}

BYTE x_time(BYTE b, BYTE n) {
	int i;
	BYTE temp = 0, mask = 0x01;

	for (i = 0; i < 8; i++) {
		if (n & mask)
			temp ^= b;

		if (b & 0x80)
			b = (b << 1) ^ 0x1B;
		else
			b <<= 1;

		mask <<= 1;
	}

	return temp;
}


x_time() 함수는 곱하려는 행렬 값에 따라 기약다항식 상에서 값을 배수해주는 함수이다.


역 MixColumns함수의 경우 4x4행렬의 역행렬을 사용한다.



키 확장




라운드 키는 128비트 기준 총 11개(첫 Addroundkey포함)이고 각 키는 4개의 의 워드로 구성되기때문에 총 44개의 워드가 필요하다.


키 확장의 순서는 이렇다.


1. 키를 4바이트씩 나누어 4개의 워드로 만든다.


2. 이전 번의 워드와 4번째전 워드를 xor하여 새 워드를 생성한다. ( 4의 배수 번째의 경우 이전 워드를 g함수에 통과시킨다.)

    

    2-1. 입력의 워드를 1바이트만큼 왼쪽 순환시프트를 한다.

  

    2-2. Subbytes에 썼던 치환을 여기서도 사용한다.


    2-3. 상수값 R과 xor하여 결과를 반환한다.


3. 워드가 44개 생성될때까지 2를 반복한다.


R상수



void KeyExpansion(BYTE key[], WORD w[]) {
	int i = 0;
	WORD temp;

	while (i < Nk) {
		w[i] = BTOW(key[4 * i], key[4 * i + 1], key[4 * i + 2], key[4 * i + 3]);
		i = i + 1;
	}

	i = Nk;

	while (i < (Nb * (Nr + 1))) {
		temp = w[i - 1];
		if (i%Nk == 0)
			temp = SubWord(RotWord(temp)) ^ Rcon[i / Nk - 1];
		else if ((Nk > 6) && (i%Nk == 4))
			temp = SubWord(temp);

		w[i] = w[i - Nk] ^ temp;
		i += 1;
	}
}

* Nk : 키 바이트수(4), Nb : 블럭 수(4), Nr : 라운드수(10)



이상 AES의 알고리즘에 대해 알아보았다...

728x90
728x90

DES 메카니즘



현대암호(하이브리드 암호)의 조상님인 DES( Data Encryption Standard )이다.


DES는 feistel 구조로 되어있으며 라운드수는 16이고 64비트 입력(블럭)에 64비트키(사실상 56비트 키)를 가진다.


대략적인 순서는


1. 64비트 입력을 초기 전치한다.


2. 32비트씩 쪼갠다.


3. 오른쪽32비트는 다음 라운드의 왼쪽32비트가 된다.


4. 오른쪽32비트를 라운드 키와함께 F함수에 통과한 후 왼쪽32비트와 xor하여 다음 라운드의 오른쪽32비트가 된다.


5. 3, 4를 16번 반복한다. 단, 마지막 16라운드에는 왼쪽 오른쪽을 교차하지 않는다.


6. 최종 전치( 초기 전치의 역전치 )를 하면 끝.


feistel구조는 원래 des이전부터 있던 구조이며 따라서 feistel구조의 블럭암호들은 보통 F함수를 설계한다.


DES F함수



DES의 F함수의 대략적 순서는


1. 32비트 입력을 48비트로 확장전치한다.


2. 48비트 라운드키와 xor한다.


3. 6비트씩 S박스에 통과시킨다. 이때 박스당 6비트의 입력은 4비트의 출력을 내며 S박스 통과 후 32비트가 된다.


4. 한번 더 전치 끝.


키는 입력 64비트키에서 확장시켜서 라운드키를 생성하는데


대략적 순서는


1. 전치한다. 이때 8의 배수 번째(8, 16, ... , 64)가 없어지며 56비트가 된다.

(이때문에 DES의 키공간은 2^56이다.)


2. 28비트씩 쪼갠다.


3. 라운드별 쉬프트 횟수만큼 왼쪽순환쉬프트를 왼쪽28비트 오른쪽28비트 각각 해준다.


4. 합쳐서 전치하여 라운드키 생성. 이때 48비트가 된다.


5. 3, 4번을 16라운드 반복한다.



코드를 보자


void DES_Encryption(BYTE *p_text, BYTE *result, BYTE *key) {
	int i;
	BYTE data[BLOCK_SIZE] = { 0, };

	BYTE round_key[16][6] = { 0, };
	UINT left = 0, right = 0;

	key_expansion(key, round_key);    // 키 확장
	IP(p_text, data);                        // 초기 전치

	BtoW(data, &left, &right);           // 32비트씩 쪼갬

	for (i = 0;i < DES_ROUND; i++) { 
		left = left ^ f(right, round_key[i]);
		if (i != DES_ROUND - 1)
			swap(&left, &right);
	}

	WtoB(left, right, data);
	In_IP(data, result);                    // 마지막 전치
}


암호화 코드이다. 


복호화 코드는 라운드 키를 역순으로 넣어주는 것 빼고 다른건 없다.( Feistel 구조의 특성 )




void IP(BYTE *in, BYTE *out)
{
	int i;
	BYTE index, bit, mask = 0x80;

	for (i = 0;i<64;i++)
	{
		index = (ip[i] - 1) / 8;
		bit = (ip[i] - 1) % 8;

		if (in[index] & (mask >> bit))
			out[i / 8] |= mask >> (i % 8);
	}
}


초기 전치 코드이다.


마스크값과 &연산을 하여 해당 비트가 1인지 확인하고 1이면 out의 비트 1로 한다.


0으로 초기화되어 있기때문에 0일때는 처리하지 않는다.




UINT f(UINT r, BYTE* rkey)
{
	int i;
	BYTE data[6] = { 0, };
	UINT out;

	EP(r, data);

	for (i = 0;i<6;i++)                                 // 라운드 키와 xor
		data[i] = data[i] ^ rkey[i];

	out = Permutation(S_box_Transfer(data));   // S박스 통과하여 전치함

	return out;
}


F함수 코드이다.



void EP(UINT r, BYTE* out)
{
	int i;
	UINT mask = 0x80000000;

	for (i = 0;i<48;i++)
	{
		if (r & (mask >> (E[i] - 1)))
		{
			out[i / 8] |= (BYTE)(0x80 >> (i % 8));
		}
	}
}


F함수 내 확장 전치 코드이다.


IP코드와 유사하다.


이 외의 전치코드들도 거의 흡사하므로 담지 않았다.



UINT S_box_Transfer(BYTE* in)
{
	int i, row, column, shift = 28;
	UINT temp = 0, result = 0, mask = 0x80;

	for (i = 0;i<48;i++)
	{
		if (in[i / 8] & (BYTE)(mask >> (i % 8)))  // 마스크를 씌워 확인 후 temp에 해당 비트 1로 함
                        temp |= 0x20 >> (i % 6);

		if ((i + 1) % 6 == 0)                        // 6비트마다
		{
			row = ((temp & 0x20) >> 4) + (temp & 0x01);           // 행 값
			column = (temp & 0x1E) >> 1;                               // 열 값

			result += ((UINT)s_box[i / 6][row][column] << shift);    // 값 더하고 쉬프트(4비트씩)

			shift -= 4;
			temp = 0;
		}
	}

	return result;
}


S-box의 코드이다.


C언어에서 최소 데이터형이 8비트이기 때문에 temp에 6비트씩 담아서 처리한다.


비트필드로도 처리해도 되겠지만 귀찮아서 패스.


6비트중 상위 1번째비트와 6번째 비트는 S-box의 행값( 따라서 총 4행 )을 나타내고


중간 4비트는 열값을 나타내며 해당 행, 열에 위치한 값으로 치환된다.


S1 box




void key_expansion(BYTE *key, BYTE round_key[16][6])
{
	int i;
	BYTE pc1_result[7] = { 0, };
	UINT c = 0, d = 0;

	PC1(key, pc1_result);                // 축소 전치(64 -> 56)

	makeBit28(&c, &d, pc1_result);  // 28비트씩 쪼갬

	for (i = 0;i<16;i++)
	{
		c = cir_shift(c, i);             // 순환왼쪽쉬프트
		d = cir_shift(d, i);

		PC2(c, d, round_key[i]);     // 축소 전치(56 -> 48)
	}
}


키 확장 코드이다.



UINT cir_shift(UINT n, int r)
{
	int n_shift[16] = { 1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1 };

	n = (n << n_shift[r]) + (n >> (28 - n_shift[r]));

	return n & 0xFFFFFFF;      // 쓰레기값 제거
}


왼쪽 순환 쉬프트 코드이다.


n_shift 배열에 각 라운드별 순환 횟수가 있다.


그 횟수 만큼 왼쪽 쉬프트를 해주고 순환쉬프트이므로 28비트에서 밀려난 상위비트를 밀려난 만큼 최하위로 밀어준다.


UINT는 32비트이므로 n을 왼쪽으로 밀때 32비트 상위 4비트에 쓰레기값이 생긴다. 따라서 하위 28비트만 취할 수 있게


0xFFFFFFF의 마스크를 씌워 반환한다.



이상 DES의 구조와 핵심 코드들을 살펴 보았다.

728x90
728x90

윤성우씨의 "열혈강의 TCP/IP 소켓 프로그래밍" 책 끝부분의 IOCP 예제를 c++형식으로 바꾸었다.


main.cpp

#include "IOCPServer.h"

int main()
{
    IOCPServer *s = new IOCPServer();

    s->Init();

    s->Run();

    delete s;
    return 0;
}


별 내용 없다.


IOCPServer.h

#pragma once

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

#define PORT        9088
#define BUFSIZE     1024
#define READ        3
#define WRITE       5

///////////////////////////////////////////////
// Structure Definition
typedef struct          // 소켓 정보를 구조체화
{
    SOCKET      hClntSock;
    SOCKADDR_IN clntAddr;
} PER_HANDLE_DATA, *LPPER_HANDLE_DATA;

typedef struct          // 소켓의 버퍼 정보를 구조체화
{
    OVERLAPPED overlapped;
    CHAR       buffer[BUFSIZE];
    WSABUF     wsaBuf;
    int        rwMode;
} PER_IO_DATA, *LPPER_IO_DATA;


////////////////////////////////////////////////
// Class definition
class IOCPServer {
public:
    IOCPServer();
    ~IOCPServer();

    bool Run();
    bool Init();
    void Cleanup();

    bool setSocket();

    static unsigned int __stdcall _CompletionThread(void *p_this);
    UINT WINAPI CompletionThread();

private:
    HANDLE  m_hCompletionPort;

    SOCKET m_hServSock;
    SOCKADDR_IN m_servAddr;
};

//////////////////////////////////////////
// Function forward declaration
void ErrorHandling(LPCSTR pszMessage);


중간에 _CompletionThread() 멤버함수가 static인 이유는 _beginthreadex() 함수의 인자때문으로


_CompletionThread()가 호출되면 바로 CompletionThread() 멤버함수를 호출한다.


IOCPServer.cpp

#include "IOCPServer.h" IOCPServer::IOCPServer() { } IOCPServer::~IOCPServer() { closesocket(m_hServSock); WSACleanup(); } bool IOCPServer::Init() { WSAData wsaData; SYSTEM_INFO systemInfo; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { // Load Winsock 2.2 DLL ErrorHandling("WSAStartup() error!"); } // Completion Port 생성 m_hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); GetSystemInfo(&systemInfo); // 쓰레드를 CPU 개수*2 만큼 생성 for (int i = 0; i<systemInfo.dwNumberOfProcessors*2; i++) { _beginthreadex(NULL, 0, _CompletionThread, (LPVOID)this, 0, NULL); } setSocket(); return true; } void IOCPServer::Cleanup() { } bool IOCPServer::setSocket() { // IOCP를 사용하기 위한 소켓 생성 (Overlapped I/O 소켓, 윈도우용) m_hServSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED); if (m_hServSock == INVALID_SOCKET) { ErrorHandling("WSASocket() error!"); } // 바인딩할 소켓 주소정보 memset(&m_servAddr, 0, sizeof(m_servAddr)); m_servAddr.sin_family = AF_INET; m_servAddr.sin_addr.s_addr = htonl(INADDR_ANY); m_servAddr.sin_port = htons(PORT); if (bind(m_hServSock, (SOCKADDR *)&m_servAddr, sizeof(m_servAddr)) == SOCKET_ERROR) { ErrorHandling("bind() error!"); } if (listen(m_hServSock, 10) == SOCKET_ERROR) { ErrorHandling("listen() error!"); } return true; } bool IOCPServer::Run() { LPPER_IO_DATA perIoData; LPPER_HANDLE_DATA perHandleData; DWORD dwRecvBytes; DWORD i, dwFlags; while (TRUE) { SOCKET hClntSock; SOCKADDR_IN clntAddr; int nAddrLen = sizeof(clntAddr); hClntSock = accept(m_hServSock, (SOCKADDR *)&clntAddr, &nAddrLen); if (hClntSock == INVALID_SOCKET) { ErrorHandling("accept() error!"); } // 연결된 클라이언트의 소켓 핸들 정보와 주소 정보를 설정 perHandleData = new PER_HANDLE_DATA; perHandleData->hClntSock = hClntSock; memcpy(&(perHandleData->clntAddr), &clntAddr, nAddrLen); // 3. Overlapped 소켓과 Completion Port 의 연결 CreateIoCompletionPort((HANDLE)hClntSock, m_hCompletionPort, (DWORD)perHandleData, 0); // 클라이언트를 위한 버퍼를 설정, OVERLAPPED 변수 초기화 perIoData = new PER_IO_DATA; memset(&(perIoData->overlapped), 0, sizeof(OVERLAPPED)); perIoData->wsaBuf.len = BUFSIZE; perIoData->wsaBuf.buf = perIoData->buffer; perIoData->rwMode = READ; dwFlags = 0; // 4. 중첩된 데이타 입력 WSARecv(perHandleData->hClntSock, // 데이타 입력 소켓 &(perIoData->wsaBuf), // 데이타 입력 버퍼 포인터 1, // 데이타 입력 버퍼의 수 &dwRecvBytes, &dwFlags, &(perIoData->overlapped), // OVERLAPPED 변수 포인터 NULL ); } return true; } unsigned int __stdcall IOCPServer::_CompletionThread(void *p_this) { IOCPServer* p_IOCPServer = static_cast<IOCPServer*>(p_this); p_IOCPServer->CompletionThread(); // Non-static member function! return 0; } UINT WINAPI IOCPServer::CompletionThread() { HANDLE hCompletionPort = (HANDLE)m_hCompletionPort; SOCKET hClntSock; DWORD dwBytesTransferred; LPPER_HANDLE_DATA perHandleData; LPPER_IO_DATA perIoData; DWORD dwFlags; while (TRUE) { // 5. 입.출력이 완료된 소켓의 정보 얻음 GetQueuedCompletionStatus(hCompletionPort, // Completion Port &dwBytesTransferred, // 전송된 바이트 수 (LPDWORD)&perHandleData, (LPOVERLAPPED *)&perIoData, INFINITE); //printf("GQCS error code : %d (TID : %d)\n", GetLastError(), GetCurrentThreadId()); if (perIoData->rwMode == READ) { puts("message received!"); if (dwBytesTransferred == 0) // 전송된 바이트가 0일때 종료 (EOF 전송 시에도) { puts("socket will be closed!"); closesocket(perHandleData->hClntSock); delete perHandleData; delete perIoData; continue; } // 6. 메세지. 클라이언트로 에코 memset(&(perIoData->overlapped), 0, sizeof(OVERLAPPED)); perIoData->wsaBuf.len = dwBytesTransferred; perIoData->rwMode = WRITE; WSASend(perHandleData->hClntSock, &(perIoData->wsaBuf), 1, NULL, 0, &(perIoData->overlapped), NULL); // RECEIVE AGAIN perIoData = new PER_IO_DATA(); memset(&(perIoData->overlapped), 0, sizeof(OVERLAPPED)); perIoData->wsaBuf.len = BUFSIZE; perIoData->wsaBuf.buf = perIoData->buffer; perIoData->rwMode = READ; dwFlags = 0; WSARecv(perHandleData->hClntSock, &(perIoData->wsaBuf), 1, NULL, &dwFlags, &(perIoData->overlapped), NULL); } else // WRITE Mode { puts("message sent!"); delete perIoData; } } return 0; } void ErrorHandling(LPCSTR pszMessage) { fputs(pszMessage, stderr); fputc('\n', stderr); exit(1); }


CreateIoCompletionPort() 함수는 특이하게도 이름처럼 completion port(이하 cp)를 생성하는데도 쓰지만


cp에 소켓을 등록하는데에도 쓰인다.


Run() 멤버 함수는 호출되면 클라이언트의 연결을 받아들이고 cp에 등록 후 WSARecv()를 호출하는 과정을 반복한다.


그리고 CompletionThread() 에서 각 스레드는 GetQueuedCompletionStatus() 를 호출하고 입출력이 완료된 소켓이


큐에 올라올때까지 대기한다. 입출력이 완료된(입력이 완료된) 소켓이 올라오면 받은 데이터를 그대로 WSASend()를 이용하여 보내주고


다시 WSARecv()호출한다.


실행한 모습

728x90

'프로그래밍 > C++' 카테고리의 다른 글

[C++] Reference Counting  (2) 2020.11.04
힙(Heap)과 완전 이진 트리(Complete binary tree)  (1) 2019.10.31
[c] SHA-1 구조 및 코드  (2) 2017.12.09
[c] AES 구조 및 코드  (2) 2017.11.15
[c] DES 구조 및 코드  (21) 2017.10.26

+ Recent posts