시작하며
지금까지는 이 소켓 프로그램에서 멀티쓰레드, select 함수 등, 실제 소켓 프로그래밍에 대한 지식들에 대해 설명하였고, 이번에는 어떻게 암호화된 통신을 구현하였는지에 대해 다루겠다. 보안은 언제나 중요한 요소로써, 데이터를 주고 받을 때, 암호화하지 않고 보낸다면 네트워크 패킷에 어떤 데이터를 보내는지 그대로 노출돼 기밀성을 보장할 수 없다. 즉 기밀성이 요구되는 데이터라면 반드시 암호화해서 전송하고, 복호화해서 읽어야할 것이다. 이 소켓은 오픈채팅 형식의 채팅 프로그램으로써, 각 클라이언트들은 메시지를 암호화하여 주고받으므로 외부에서는 메시지를 읽을 수 없다. 어떻게 구현하였는지 살펴보자.
1. TLS와 키교환에 대한 이야기
TLS 는 현대 암호화에 엄청난 정수가 담겨있다고 생각한다. 사실 이 코드가 실제 TLS와 완전히 동일한 방식으로 동작하진 않지만, TLS의 목적에는 부합한다. TLS에는 인증서를 포함해서 보안에 관한 굉장히 많은 내용들이 들어가 있다. 이 프로그램이 인증서 개념이나 세션의 개념은 갖고 있지 않지만 "키교환" 기능을 수행한다. 사실상 TLS의 가장 큰 목적 중 하나는 대칭키를 안전하게 나눠 갖는 것이다. 대칭키란 암호화와 복호화에 사용되는 키가 동일한 것을 의미한다. 즉 A와 B가 서로 같은 대칭키를 공유하고 있다면 이 대칭키 하나만을 이용하여 서로 데이터를 암호화해서 주고 복호화해서 읽을 수 있게된다. 가장 큰 문제는 "대칭키를 공유" 하고 있어야 한다는 것이다. 즉 둘 다 같은 대칭키를 갖기 위해서는 한 쪽이 대칭키를 만들고 보내주어야 한다. 이 보내주는 과정에서 만약 공격자가 이 전송되는 패킷을 읽을 수 있다면 공격자가 대칭키를 알게되고, 주고 받는 모든 데이터를 복호화할 수 있게 되어 기밀성을 보장받지 못하게 될 것이다.
그렇다면 어떻게 해야할까? 필자는 그냥 단순하게, "그럼 그냥 비대칭키로 암복호화하면 되지 않나" 라는 생각을 하였다. 비대칭키에서 비밀키는 네트워크를 타지 않고 오직 자신만 알며, 공개키는 공개되어도 아무런 상관이 없다. 즉 A가 B에게 메시지를 암호화하여 보내고 싶다면, B의 공개키로 메시지를 암호화하고 B는 자신의 비밀키로 복호화하면 된다. B의 비밀키로만 이 암호화된 메시지를 복호화할 수 있기 때문에, 공격자는 패킷을 가로챈다해도 이 메시지에서 원문을 구할 수는 없을 것이다. 그러나 문제가 있다. RSA같은 비대칭키 알고리즘은 너무 연산이 비효율적이라는 것이다. RSA의 키 길이와, 암복호화 과정을 생각해본다면 쉽게 이해가 될 것이다. 그렇게 긴 길이의 키에 연산은 지수연산... 대칭키에 비해 너무 연산 효율이 떨어진다는 것이 비대칭키로 암호화하는 것에 단점이며, 네트워크에서 수많은 데이터가 왔다갔다 할 때, 이를 RSA로 암호화해서 전송한다면 엄청나게 네트워크가 느려질 것이다. 결국 통신에서 데이터를 주고 받을 때 사용하는 암호화 방식은 대칭키 방식이 적절하며, 때문에 안전하게 대칭키를 서로 공유하는 방법의 필요성이 대두된다.
즉 대칭키를 안전하게 주고받는 것이 제 1의 목표이다. 어떻게 하면될까? 방법은 생각보다 간단하다.
암호화 통신에 사용할 대칭키를 비대칭키로 암호화하여 공유하면 된다.
즉 일단 대칭키만 안전하게 주고 받았다면 사실상 모든 문제가 해결된다.(물론 세션등 여러 개념이 동반되어야하지만) 대칭키는 서로 연결할 때 한번만 전송해주면 되므로, 이 딱 한번에 RSA와 같은 비대칭키를 사용하는 것이다. RSA는 대칭키를 공유할 때만 사용하고 그 이후에는 공유한 대칭키로 암호화를 진행하기 때문에, 속도 저하, 효율 저하같은 문제에서 벗어나고, 안전하게 대칭키를 공유할 수 있게 된다. 실제 TLS 동작방식을 보면 서버가 인증서에 공개키를 포함하여 전송하고, 클라이언트가 이 공개키로 대칭키를 암호화 해 전송하면 서버는 자신의 개인키로 이를 복호화하여 키를 교환하게 된다. 즉 비대칭키는 효율이 떨어져 모든 데이터 통신에 사용하기 부적합하고, 대칭키 방식은 안전하게 키를 교환해야만 하기 때문에, 이 교환할 때만 비대칭키를 사용하고 이를 통해 안전하게 대칭키를 공유해 대칭키 암호화 방식으로 통신을 진행하는 것이다. 아래는 나무위키임에도 TLS에 대한 설명이 잘 되어있어 가져온 링크이다.
TLS - 나무위키
국내 웹사이트를 TLS 접속하면 보안 경고 웹페이지 뜨는 이유가 대부분(특히 관공서에 접속할 경우) 국내인증기관에서 발급하는 인증서가 제 3자 검증(보안 감사)를 제대로 못 받았기 때문이다.
namu.wiki
2. 이 프로그램에서 키교환 방식
위에서 대칭키를 비대칭키로 암호화하여 공유한다는 것을 알아보았다. 그런데 보통, 서버와 개별 클라이언트마다 각각의 세션키를 공유하지만, 이 프로그램은 오픈채팅의 형식으로, 조금은 다르게 키교환을 한다. 간략하게 설명하면, 모든 클라이언트가 똑같은 대칭키를 공유하도록 설계하였다. 예를 들어 10명이 채팅할 때, 10명 모두 서버와 각각의 세션키를 생성한다면, 서버가 해당 클라이언트 수만큼 대칭키를 갖고 있어야하며, 한명이 데이터를 보내면, 그 송신자와의 세션키로 복호화, 그리고 받는 나머지 9명의 각각의 세션키로 9번 암호화하여 전송, 각각의 클라이언트는 자신의 대칭키로 복호화해야하는데, 이렇게 되면 서버 구현이 복잡해지고 느려지기 때문에, 각 클라이언트마다 개별 대칭키를 생성하는 것이 아니라, 모든 클라이언트가 동일한 대칭키를 갖도록 하였다. 즉 TLS와 살짝 다르다.
이 프로그램에선 서버가 대칭키를 갖고 있고 각각의 클라이언트는 자기 자신만의 비대칭키 암호화에 사용할 키 쌍을 갖고 있다는 것을 전제로 한다. 처음 소켓 연결에 성공하면,
1. 클라이언트는 자신의 공개키를 전송하고
2. 서버는 이 공개키를 이용해 대칭키를 암호화하여 다시 클라이언트에게 전송
3. 클라이언트는 자신의 공개키로 암호화된 대칭키를 자신의 개인키로 복호화하여 대칭키를 획득
4. 클라이언트는 공유받은 대칭키를 이용해 메시지를 암호화해 서버로 전송
5. 서버는 암호화된 메시지를 보낸 클라이언트를 제외한 모든 참여자 클라이언트에게 전송
6. 각각의 클라이언트는 서버로부터 받은 암호화된 메시지를 공유받은 대칭키를 이용하여 복호화
하는 방식으로 키 교환을 하며, 서버는 동일한 대칭키를 각각의 클라이언트의 공개키로 암호화하여 보내고, 클라이언트는 자신만의 개인키로 복호화하면서 안전하게 동일한 대칭키를 서버가 모든 클라이언트에게 공유할 수 있게 된다. 이런 방식을 사용하면 하나의 대칭키로 모든 클라이언트가 암호화된 통신을 할 수 있게 된다. 이제 코드를 보자.
3. RSA관련 함수
//RSA 암복호화를 위한 함수
//코드출처 : https://gaeko-security-hack.tistory.com/126
int padding = RSA_PKCS1_PADDING;//패딩방식
I_LOCAL RSA * createRSA(unsigned char * key, int public){
RSA *rsa= NULL;
BIO *keybio ;
keybio = BIO_new_mem_buf(key, -1); // 읽기 전용 메모리 만들기 BIO
if (keybio==NULL){
printf( "Failed to create key BIO");
return 0;
}
/* PEM형식인 키 파일을 읽어와서 RSA 구조체 형식으로 변환 */
if(public){ // PEM public 키로 RSA 생성
rsa = PEM_read_bio_RSA_PUBKEY(keybio, &rsa, NULL, NULL);
}
else{ // PEM private 키로 RSA 생성
rsa = PEM_read_bio_RSAPrivateKey(keybio, &rsa, NULL, NULL);
}
if(rsa == NULL)
printf( "Failed to create RSA");
return rsa;
}
/* 공개키로 암호화 */
I_EXPORT int public_encrypt(unsigned char * data, int data_len, unsigned char * key, unsigned char *encrypted) {
RSA * rsa = createRSA(key,1);
int result = RSA_public_encrypt(data_len, data, encrypted, rsa, padding);
return result; // RSA_public_encrypt() returns the size of the encrypted data
}
/* 개인키로 복호화 */
I_EXPORT int private_decrypt(unsigned char * enc_data, int data_len, unsigned char * key, unsigned char *decrypted){
RSA * rsa = createRSA(key,0);
int result = RSA_private_decrypt(data_len, enc_data, decrypted, rsa,padding);
return result;
}
해당 코드는 I_CRYPTO 프로젝트에서 Socket 경로에 있는 i_crypto_library에 추가되어있는 rsa관련함수이다. 해당 함수들은 실제로 구현한 것이 아닌 아래 링크에서 그대로 가져왔다.
https://gaeko-security-hack.tistory.com/126
[암호] openssl를 이용한 RSA 암복호화
openssl을 이용한 RSA 암복호화해주는 소스코드를 분석해보았다. 코드는 다음과 같다. #include #include #include #include #include #include #include int padding = RSA_PKCS1_PADDING; RSA * createRSA(unsigned char * key,int public){
gaeko-security-hack.tistory.com
이 프로젝트에서 createRSA 함수는 사실상 사용하지 않고, public_encrypt, private_decrypt 함수만 사용한다.
4. Client 키교환
int main(int argc, char* argv[]){
int ret = 0;
ClientSockInfo client;
struct sockaddr_in serv_addr;
pthread_t send_thread, recv_thread;
void* thread_return;
uint8_t public_key[] = "-----BEGIN PUBLIC KEY-----\n"\
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy8Dbv8prpJ/0kKhlGeJY\n"\
"ozo2t60EG8L0561g13R29LvMR5hyvGZlGJpmn65+A4xHXInJYiPuKzrKUnApeLZ+\n"\
"vw1HocOAZtWK0z3r26uA8kQYOKX9Qt/DbCdvsF9wF8gRK0ptx9M6R13NvBxvVQAp\n"\
"fc9jB9nTzphOgM4JiEYvlV8FLhg9yZovMYd6Wwf3aoXK891VQxTr/kQYoq1Yp+68\n"\
"i6T4nNq7NWC+UNVjQHxNQMQMzU6lWCX8zyg3yH88OAQkUXIXKfQ+NkvYQ1cxaMoV\n"\
"PpY72+eVthKzpMeyHkBn7ciumk5qgLTEJAfWZpe4f4eFZj/Rc8Y8Jj2IS5kVPjUy\n"\
"wQIDAQAB\n"\
"-----END PUBLIC KEY-----\n";
uint32_t public_keylength = strlen(public_key);
uint8_t private_key[] = "-----BEGIN RSA PRIVATE KEY-----\n"\
"MIIEowIBAAKCAQEAy8Dbv8prpJ/0kKhlGeJYozo2t60EG8L0561g13R29LvMR5hy\n"\
"vGZlGJpmn65+A4xHXInJYiPuKzrKUnApeLZ+vw1HocOAZtWK0z3r26uA8kQYOKX9\n"\
"Qt/DbCdvsF9wF8gRK0ptx9M6R13NvBxvVQApfc9jB9nTzphOgM4JiEYvlV8FLhg9\n"\
"yZovMYd6Wwf3aoXK891VQxTr/kQYoq1Yp+68i6T4nNq7NWC+UNVjQHxNQMQMzU6l\n"\
"WCX8zyg3yH88OAQkUXIXKfQ+NkvYQ1cxaMoVPpY72+eVthKzpMeyHkBn7ciumk5q\n"\
"gLTEJAfWZpe4f4eFZj/Rc8Y8Jj2IS5kVPjUywQIDAQABAoIBADhg1u1Mv1hAAlX8\n"\
"omz1Gn2f4AAW2aos2cM5UDCNw1SYmj+9SRIkaxjRsE/C4o9sw1oxrg1/z6kajV0e\n"\
"N/t008FdlVKHXAIYWF93JMoVvIpMmT8jft6AN/y3NMpivgt2inmmEJZYNioFJKZG\n"\
"X+/vKYvsVISZm2fw8NfnKvAQK55yu+GRWBZGOeS9K+LbYvOwcrjKhHz66m4bedKd\n"\
"gVAix6NE5iwmjNXktSQlJMCjbtdNXg/xo1/G4kG2p/MO1HLcKfe1N5FgBiXj3Qjl\n"\
"vgvjJZkh1as2KTgaPOBqZaP03738VnYg23ISyvfT/teArVGtxrmFP7939EvJFKpF\n"\
"1wTxuDkCgYEA7t0DR37zt+dEJy+5vm7zSmN97VenwQJFWMiulkHGa0yU3lLasxxu\n"\
"m0oUtndIjenIvSx6t3Y+agK2F3EPbb0AZ5wZ1p1IXs4vktgeQwSSBdqcM8LZFDvZ\n"\
"uPboQnJoRdIkd62XnP5ekIEIBAfOp8v2wFpSfE7nNH2u4CpAXNSF9HsCgYEA2l8D\n"\
"JrDE5m9Kkn+J4l+AdGfeBL1igPF3DnuPoV67BpgiaAgI4h25UJzXiDKKoa706S0D\n"\
"4XB74zOLX11MaGPMIdhlG+SgeQfNoC5lE4ZWXNyESJH1SVgRGT9nBC2vtL6bxCVV\n"\
"WBkTeC5D6c/QXcai6yw6OYyNNdp0uznKURe1xvMCgYBVYYcEjWqMuAvyferFGV+5\n"\
"nWqr5gM+yJMFM2bEqupD/HHSLoeiMm2O8KIKvwSeRYzNohKTdZ7FwgZYxr8fGMoG\n"\
"PxQ1VK9DxCvZL4tRpVaU5Rmknud9hg9DQG6xIbgIDR+f79sb8QjYWmcFGc1SyWOA\n"\
"SkjlykZ2yt4xnqi3BfiD9QKBgGqLgRYXmXp1QoVIBRaWUi55nzHg1XbkWZqPXvz1\n"\
"I3uMLv1jLjJlHk3euKqTPmC05HoApKwSHeA0/gOBmg404xyAYJTDcCidTg6hlF96\n"\
"ZBja3xApZuxqM62F6dV4FQqzFX0WWhWp5n301N33r0qR6FumMKJzmVJ1TA8tmzEF\n"\
"yINRAoGBAJqioYs8rK6eXzA8ywYLjqTLu/yQSLBn/4ta36K8DyCoLNlNxSuox+A5\n"\
"w6z2vEfRVQDq4Hm4vBzjdi3QfYLNkTiTqLcvgWZ+eX44ogXtdTDO7c+GeMKWz4XX\n"\
"uJSUVL5+CVjKLjZEJ6Qc2WZLl94xSwL71E41H4YciVnSCQxVc4Jw\n"\
"-----END RSA PRIVATE KEY-----\n";
uint32_t private_keylength = strlen(private_key);
uint8_t encKey[256];
uint32_t encKeylength = 0;
uint32_t keylength = 0;
int msg_length = 0;
//init client
init_ClientSockInfo(&client);
client.param.mode = I_CIPHER_MODE_CBC;
if(argc != 4){//경로/ip/port/name
printf("Usage : %s <IP> <port> <name> \n", argv[0]);
exit(EXIT_FAILURE);
}
//이름과 소켓을 구조체에 저장장
sprintf(client.name, "[%s]", argv[3]);
client.sock = socket(PF_INET, SOCK_STREAM, 0);
//init server info
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port=htons(atoi(argv[2]));
//연결
connect(client.sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//연결에 성공하면 서버에 공개키의 길이와 공개키를 보냄
msg_length = htonl(public_keylength);
send(client.sock, &msg_length, sizeof(msg_length), 0);
send(client.sock, public_key, public_keylength, 0);
//자신이 보낸 공개키로 암호화된 대칭키를 서버로부터 획득
msg_length = 0;
recv(client.sock, &msg_length, sizeof(msg_length), 0);
msg_length = ntohl(msg_length);
encKeylength = recv_all(msg_length, client.sock, encKey);
if(encKeylength == -1 || encKeylength != msg_length){
printf("main error, encKeylength : %d, msg_length : %d\n", encKeylength, msg_length);
close(client.sock);
return -1;
}
hexdump("encKey", encKey, encKeylength);
//암호화된 대칭키를 자신의 개인키로 복호화하여 최종적으로 대칭키 획득
recv(client.sock, &keylength, sizeof(keylength), 0);//원본 대칭키의 길이를 받음
keylength = ntohl(keylength);
ret = private_decrypt(encKey, encKeylength, private_key, client.key);
if(ret==-1 || ret != keylength){
printf("failed dec key\n");
printf("ret %d, key %d\n", ret, keylength);
close(client.sock);
return ret;
}
hexdump("key", client.key, 16);
//쓰레드 생성
pthread_create(&send_thread, NULL, send_msg, (void*)&client);
pthread_create(&recv_thread, NULL, recv_msg, (void*)&client);
pthread_join(send_thread, &thread_return);
if(client.flag == 1){
printf("연결을 종료합니다.\n");
pthread_cancel(recv_thread);
}
pthread_join(recv_thread, &thread_return);
close(client.sock);
return 0;
}
먼저 구현의 편의때문에 rsa 키쌍은 코드에 그대로 넣어놓았다.(실제로 이렇게 코딩하면 큰일난다) 해당 코드의 주석을 따라가면서 설명을 읽어야 이해가 쉬울 것이다. 1. 먼저 클라이언트는 서버와 연결되면 공개키의 길이를 보내고 공개키를 보낸다.(앞선 글에서 데이터의 길이를 먼저 보내야한다는 것은 여러번 설명하였으므로 아래 설명부터는 길이를 보내는 것에 대한 언급은 생략하겠다.) 2. 그러면 서버는 클라이언트의 공개키로 대칭키를 암호화하여 전송할 것이므로, 클라이언트는 자신의 공개키로 암호화된 대칭키를 수신한다. 3. 이 대칭키를 자신의 비밀키로 복호화한다. 이 때, 서버로부터 실제 대칭키의 길이를 받아 자신이 복호화한 대칭키와 길이가 맞는지 확인한다. 앞서 설명한 키교환 을 이런식으로 구현하였다. 실제로는 원래 tls에서는 키를 제대로 공유하였는지 확인하는 절차가 있지만 여기서는 구현하지 않았다.
5. Client : send_msg, recv_msg
void* send_msg(ClientSockInfo* client){
char msg[BUF_SIZE] = {0x00, };
char name_msg[NAME_SIZE + BUF_SIZE] ={0x00, };//buffer for sending message with name
AES_KEY encKey;
//clinet = (CliencSockInfo*)client;
uint8_t encData[256] = {0x00, };
uint32_t encDataLength = 0;
uint32_t msg_length = 0;//메세지를 보내기 전, 메시지 크기를 보내기 위한 변수(htonl()로 빅엔디안으로 크기를 담음)
//대칭키 생성
if(AES_set_encrypt_key(client->key, 128, &(encKey)) < 0){
printf("key 생성 오류\n");
return 0;
}
//처음 연결 성공하면 연결 메시지 보냄
sprintf(name_msg, "%s %s", client->name, "Connected...\n");
i_enc(cipher_id, &encKey, &(client->param), name_msg, strlen(name_msg), encData, &encDataLength);
msg_length = htonl(encDataLength);//보낼 데이터의 크기를 빅엔디안으로 저장
write(client->sock, &msg_length, sizeof(msg_length));//크기부터 보냄
write(client->sock, encData, encDataLength);//크기를 보낸 후 메세지를 보냄
while(1){
msg_length = 0;
memset(msg, 0 , BUF_SIZE);
memset(name_msg, 0, NAME_SIZE+BUF_SIZE);
memset(encData, 0, 256);
encDataLength = 0;
printf("-> ");
fgets(msg, BUF_SIZE, stdin);
//q or Q를 입력받으면 클로즈 메시지 보내고 소켓 종료
if(!strcmp(msg, "q\n") || !strcmp(msg, "Q\n")){
sprintf(name_msg, "%s %s", client->name, "Closed...\n");
i_enc(cipher_id, &encKey, &(client->param), name_msg, strlen(name_msg), encData, &encDataLength);
msg_length = htonl(encDataLength);
write(client->sock, &msg_length, sizeof(msg_length));
write(client->sock, encData, encDataLength);
client->flag = 1;
break;
}
sprintf(name_msg, "%s %s", client->name, msg);
i_enc(cipher_id, &encKey, &(client->param), name_msg, strlen(name_msg), encData, &encDataLength);
msg_length = htonl(encDataLength);
write(client->sock, &msg_length, sizeof(msg_length));
write(client->sock, encData, encDataLength);
}
return NULL;
}
위 과정에서 대칭키를 획득하면 이 대칭키를 이용해 앞서 직접 구현하였던 i_enc함수를 이용하여 암호화(이 코드에선 cbc)를 하여 메시지를 전송한다. 먼저 처음 연결에 성공하면 "{닉네임} Connected..." 메시지를 암호화하여 전송한다. 한가지 알아두어야 할 것은 키교환 이후 모든 메시지는 암호화되어서 보내진다는 점이다. 그 후 while문으로 들어가 q혹은 Q를 입력받을 때까지 메시지를 계속 암호화하여 전송한다. 종료 문자를 입력받으면 "{닉네임} Closed..."를 전송하고 함수를 종료한다.
void* recv_msg(ClientSockInfo* client){
char name_msg[NAME_SIZE+BUF_SIZE] = {0x00,};
int msg_length = 0;
int str_len = 0;
AES_KEY decKey;
uint8_t decData[256] = {0x00,};
uint32_t decDataLength = 0;
//대칭키 생성
if(client->param.mode == I_CIPHER_MODE_CTR){//ctr모드이면 encrypt키로 키 설정
if(AES_set_encrypt_key(client->key, 128, &decKey) < 0){
printf("decKey 생성 오류\n");
return 0;
}
}
else{
if(AES_set_decrypt_key(client->key, 128, &decKey) < 0){
printf("encKey 생성 오류\n");
return 0;
}
}
//recv msg...
while(1){
read(client->sock, &msg_length, sizeof(msg_length));
msg_length = ntohl(msg_length);
str_len = read_all(msg_length, client->sock, name_msg);
if(str_len == -1 || str_len != msg_length){
printf("recv_msg()에러 str_len = %d, msg_length = %d\n", str_len, msg_length);
return (void*)-1;
}
i_dec(cipher_id, &decKey, &(client->param), name_msg, str_len, decData, &decDataLength);
decData[decDataLength] = '\0';
fputs(decData, stdout);
memset(name_msg, 0, NAME_SIZE+BUF_SIZE);
memset(decData, 0, decDataLength);
decDataLength = 0;
msg_length = 0;
str_len = 0;
}
return NULL;
}
recv_msg함수는 키설정 부분 말고 while문만 보자. 메시지를 서버로부터 전송받으면 i_dec 함수를 이용해 이를 복호화하고 화면에 출력하는 역할을 수행한다.
다시 한번 짚고 넘어갈건, 이 프로그램은 10명이 채팅한다 했을 때, 한 명이 메시지를 보내면 이 메시지가 먼저 서버로가고, 서버가 나머지 9명한테 메시지를 뿌려주는 것이다. 즉 개별 클라이언트가 받는 모든 메시지는 상대 클라이언트가 보낸거여도 자신이 수신받는 곳은 서버라는 것을 알아두자. 이제 서버 코드를 확인해보자.
6. 서버 키교환
int main(int argc, char* argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_addr, clnt_addr;
socklen_t clnt_addr_size;
pthread_t t_id;
int ret = 0;
int cipher_id = I_CIPHER_ID_AES128;
uint8_t key[16] = {0x01, 0x02, 0x03, 0x04,
0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c,
0x0d, 0x0e, 0x0f, 0x10};
uint32_t keylength = 16;
uint8_t encKey[256] = {0x00,};
uint32_t encKeylength = 0;
uint8_t client_public_key[2048] = {0x00,};
uint32_t client_public_keylength = 0;
if(argc!=2){//경로/port번호 입력받아야함
printf("Usage : %s <port> \n", argv[0]);
exit(EXIT_FAILURE);
}
pthread_mutex_init(&mutx, NULL);//mutex생성
serv_sock = socket(PF_INET, SOCK_STREAM, 0); //소켓 생성
//init server
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_addr.sin_port = htons(atoi(argv[1]));
//소켓에 주소할당
bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
//listen
listen(serv_sock, 5);
while(1){
int msg_length = 0;
//accept client, and create new socket
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
printf("Connected IP : %s\n", inet_ntoa(clnt_addr.sin_addr));
//클라이언트의 공개키를 받음
recv(clnt_sock, &msg_length, sizeof(msg_length), 0);
msg_length = ntohl(msg_length);
client_public_keylength = recv_all(msg_length, clnt_sock, client_public_key);
if(msg_length != client_public_keylength || client_public_keylength == -1){
printf("main error, client_public_keylength : %d, msg_length : %d\n", client_public_keylength, msg_length);
close(clnt_sock);
return -1;
}
hexdump("client_public_key", client_public_key, client_public_keylength);
//클라이언트의 공개키로 대칭키를 암호화
ret = public_encrypt(key, keylength, client_public_key, encKey);
if(ret == -1){
printf("rsa enc failed...\n");
close(clnt_sock);
return ret;
}
encKeylength = ret;
//I_asym_enc(client_public_key, client_public_keylength, ¶m, key, keylength, encKey, &encKeylength);
hexdump("key", key, keylength);
hexdump("encKey", encKey, encKeylength);
//암호화한 대칭키 전송
msg_length = 0;
msg_length = htonl(encKeylength);
send(clnt_sock, &msg_length, sizeof(msg_length), 0);
send(clnt_sock, encKey, encKeylength, 0);
//실제 대칭키의 크기를 클라이언트에게 전송
msg_length = 0;
msg_length = htonl(keylength);
send(clnt_sock, &msg_length, sizeof(msg_length), 0);
//client 갯수 증가(mutex처리)
pthread_mutex_lock(&mutx);
clnt_socks[clnt_cnt++] = clnt_sock;
pthread_mutex_unlock(&mutx);
//create Thread...
pthread_create(&t_id, NULL, handle_clnt, (void*)&clnt_sock);
pthread_detach(t_id);
}
return 0;
}
먼저 서버에 key라는 이름의 변수로 16바이트 대칭키를 그대로 코드에 넣어두었다.(당연히 이것도 이렇게 하면 안된다.) 1. 서버는 클라이언트와 연결되면 클라이언트로 공개키를 받고 2. 클라이언트의 공개키로 대칭키를 암호화한다. 3. 그리고 이 암호화한 대칭키를 클라이언트로 전송하며 4. 클라이언트가 키 길이로 키를 잘 복호화했는지 검증할 수 있도록, 실제 대칭키 크기(여기서는 16바이트)를 한번 더 전송해준다.
이 프로그램에서, 서버도 당연히 서버가 대칭키를 주는 것이기 때문에 모든 메시지를 복호화해 볼 수 있으나, 서버는 따로 메시지를 복호화하지 않고 그대로 다른 클라이언트들에게 브로드캐스팅 해준다. 따라서 서버는 키교환 이후 따로 데이터를 암복호화하지 않으므로 데이터를 주고 받는 코드에 대한 설명은 생략하고 실행 결과를 확인해보겠다.
7. 실행결과
위는 클라이언트가 처음 접속했을 때 서버가 출력하는 키교환 과정이다. 처음 client의 퍼블릭 키를 받아 화면에 출력하고, 자신이 갖고 있는 대칭키를 출력, 이를 클라이언트의 공개키로 암호화한 데이터 출력, 그 후 이를 클라이언트에 보내게 된다. 클라이언트 화면을 보자.
클라이언트는 접속하면 자신의 공개키로 암호화한 대칭키를 서버로 부터 받으면 이를 화면에 출력, 또 복호화한 대칭키를 출력한다. 위의 서버 사진과 비교해보면, 복호화한 키가 서버가 주는 대칭키와 정확히 일치하는 것을 볼 수 있다.
그 후 왼쪽은 클라이언트가 메시지를 입력했을 때 오른쪽 서버에 복호화되지 않고 암호화된 상태로 출력되는 것을 볼 수 있다. 다음은 실제 3명의 클라이언트가 채팅하는 과정이다.
왼쪽 위는 서버로, 계속 암호화되어 전송되는 메시지들을 출력한다. 왼쪽 밑이 kim, 오른쪽 위가 lee, 오른쪽 아래가 park이다. 메시지 옆에 닉네임이 있으면 해당 닉네임의 클라이언트가 보낸거고, ->나 아무것도 없다면 자신이 입력한 것이다. 스티커는 테스트로 본명을 입력해 가린것이니 신경안써도 된다. q를 누르면 채팅을 종료하고 소켓을 끊는다. 여기서 테스트해본 결과 한글은 잘 되긴 하는데 지울때 글자는 지워져도 보이지 않는 특수문자같은 것이 남는 것 같다. 중간중간 물음표같은 것이 찍힌 것이 한글이 제대로 지워지지 않아서 생긴 것들이다. 영어는 매우 잘된다. 이렇게 간단하게 세개의 클라이언트가 채팅하는 모습을 캡쳐해 보았다.
마치며
크립토 라이브러리 자체를 구현한 것과, 이렇게 직접 만든 라이브러리 함수를 이용해, 암호화된 통신을 하는 소켓 프로그램을 만들었다는 것에 뭔가 스스로 실력이 향상한 것을 느낄 수 있었다. 이론으로만 배웠던 보안 지식들을 직접 구현하는 것은 생각 이상으로 매우 큰 도움이 되었고, 시험기간 외우기만 해서 금방 까먹었던 것들도 직접 내 손으로 만들다 보니 어느정도 머리에 박힌 것 같다. 또 간단한 프로그램을 짜더라도 어떻게 하면 더 안전하게 만들 수 있는지 그런 방법 같은 것들을 머릿속으로 자연스럽게 떠올리고 고민하게 된 것 같다. 내가 많은 발전을 했다고 느끼는 코드이다. 이렇게 소켓에 대한 부분도 끝이 났다. 다음 글에서는 OTP를 구현한 코드에 대한 글을 쓰며 이 I_CRYPTO 프로젝트에 대한 글이 끝나게 될 것이다. 이제 마지막을 향해 달려가 보겠다.
참고자료 : https://novice-programmer-story.tistory.com/38
Ch 18. 멀티쓰레드 기반의 서버구현
모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!
novice-programmer-story.tistory.com
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 : Epilogue (1) | 2023.05.07 |
---|---|
Crypto Project : 3. OTP 프로그램 (1) | 2023.05.06 |
Crypto Project : 2-2. 소켓에서 select() 함수를 이용한 recv_all() 함수 (0) | 2023.04.26 |
Crypto Project : 2-1. 멀티쓰레드 기반 1대n 소켓 프로그램 (1) | 2023.04.25 |
Crypto Project : 1-6. crypto library makefile과 테스트 (0) | 2023.04.23 |