카이사르는 기밀문서의 알파벳 문자를 일정 자릿수만큼 떨어진 다른 알파벳 문자로 바꾸는 방식으로 암호문을 만들었던 것으로 추정된다. 예를 들면 A는 B, B는 C, C는 D로 … 그리고 다시 Z는 A로 바꾸는 식이다. 이렇게 암호화된 메세지를 받은 수신인은 암호화에 사용된 것과 같은 자릿수만큼 반대 방향으로 변환하여 해독했다.
일반적으로 암호화되지 않은 글을 plaintext(평문)라고 하고, 암호화된 글은 ciphertext(암호문), 그리고 약속된 비밀은 key(암호키)라고 부른다. 좀 더 공식적으로 표현하자면, 카이사르의 알고리즘은 plaintext의 각 문자를 일정 자릿수만큼 회전시켜서 암호화를 하는 것이다. 이것을 공식으로 만들어보면 아래와 같다.
ciphertext = (plaintext + key) % 26
사용자에게 평문 텍스트와 암호키를 입력받고, 카이사르의 암호 알고리즘에 따라 암호문으로 변환하여 출력하는 프로그램을 만든다.
$ ./caesar 1
plaintext: HELLO
ciphertext: IFMMP
$ ./caesar 13
plaintext: hello, world
ciphertext: uryyb, jbeyq
사용자가 잘못된 값을 입력하는 경우 아래와 같은 메세지를 출력한다.
$ ./caesar HELLO
Usage: ./caesar key
$ ./caesar
Usage: ./caesar key
$ ./caesar 1 2 3
Usage: ./caesar key
main
함수가 즉시 1
을 반환하도록 한다.Usage: ./caesar key
에러 메세지를 출력하고 main
함수가 1
을 반환하도록 한다.A
인 경우, ASCII 코드에서 [
가 A
에서 27 자리만큼 떨어져 있기는 하지만, [
가 되어서는 안된다. A
는 알파벳 문자 코드 안을 돌아서 는 B
가 되고, Z
는 A
가 되어야 한다.plaintext:
를 줄바꿈 없이 출력하고, 유저에게 string
타입의 인풋을 받는 프롬프트를 출력한다. (get_string
사용)ciphertext:
를 줄바꿈 없이 출력하고, 평문의 각 알파벳 문자를 k 자릿수만큼 변환한 암호문을 출력한다. 알파벳이 아닌 문자는 변환하지 않고 출력한다.bool checkArg(int argc, char **argv)
{
if (argc != 2 || strspn(argv[1], "0123456789") != strlen(argv[1]))
{
return false;
}
return true;
}
for
루프로 argv[1]의 요소를 처음부터 하나씩 isdigit(argv[1][i])가 0인지 검사하는 방식이었다. isdigit()의 값이 0인 경우 인수가 십진 숫자가 아닌 것이므로 checkArg()는 false를 반환한다.strspn()
함수를 발견해서 코드를 수정했더니 훨씬 간결해졌다. 라이브러리의 함수를 사용하면 코드가 간결해지고, 가독성이 좋아지고, 실수를 방지하며, 재사용이 용이하다는 등의 장점이 있다고 한다. 라이브러리에 지나치게 의존하면 좋지 않다고 하지만 기본적인 라이브러리는 잘 활용하는 것이 좋은 것 같다.strspn(string1, string2)
은 string1이 string2에 사용된 문자들로 구성되어 있는지 검사하는 함수다. string1에서 string2에 없는 문자가 처음으로 발견된 위치를 반환한다. string1이 string2에 없는 문자로 시작되는 경우 함수는 0을 반환한다. string1의 모든 문자가 string2에 사용된 문자들로 구성된 경우 string1의 길이를 반환한다.과제 안내문에 제시된 카이사르의 암호화 공식은 ciphertext = (plaintext + key) % 26 였는데, 문제는 ASCII 표에서 알파벳 문자의 위치가 0부터 시작 되는 것이 아닌데다, 소문자와 대문자를 구분해야 하는 것이었다.
내 코드는 아래와 같다. if
조건문에서 인수 문자가 소문자인 경우와 대문자인 경우를 구분해서 리턴값을 다르게 했다.
char encrypt(int key, char character)
{
int position;
if (isupper(character))
{
position = (character - 65 + key) % 26;
return 65 + position;
}
if (islower(character))
{
position = (character - 97 + key) % 26;
return 97 + position;
}
return character;
}
아래는 ChatGPT에게 encrypt()의 최적화를 요구한 코드다. isalpha()
와 삼항 연산자를 이용해서 코드를 축약했다.
char encrypt(int key, char character)
{
if (isalpha(character))
{
return (char)((character - (islower(character) ? 97 : 65) + key) % 26 + (islower(character) ? 97 : 65));
}
return character;
encrypt() 함수로 암호화한 문자들을 저장한 문자열을 반환하는 getCiphertext() 함수를 만들었다. C언어는 string 타입이 없고 char의 배열로 문자열을 만든다. 즉 문자열을 저장하기 위해 고정된 크기의 데이터 타입이 없기 때문에, 사용자의 입력에 따라 문자열의 크기가 달라지는 경우에는 ‘메모리의 동적 할당’이 필요하다.
동적 메모리 할당 또는 메모리 동적 할당은 컴퓨터 프로그래밍에서 실행 시간 동안 사용할 메모리 공간을 할당하는 것을 말한다. 사용이 끝나면 운영체제가 쓸 수 있도록 반납하고 다음에 요구가 오면 재 할당을 받을 수 있다. 이것은 프로그램이 실행하는 순간 프로그램이 사용할 메모리 크기를 고려하여 메모리의 할당이 이루어지는 정적 메모리 할당과 대조적이다[2].
malloc()
을 사용한 코드다.char *getCiphertext(int key, char *plaintext)
{
size_t len = strlen(plaintext);
char *result = malloc(len);
for (int i = 0; i < (int)len; i++)
{
result[i] = encrypt(key, plaintext[i]);
}
return result;
}
strdup()
을 사용한 코드다.char *getCiphertext(int key, char *plaintext)
{
size_t len = strlen(plaintext);
char *result = strdup(plaintext);
for (int i = 0; i < (int)len; i++)
{
result[i] = encrypt(key, plaintext[i]);
}
return result;
}
strdup()
은 malloc()
과 strcpy()
를 차례로 실행하여 힙 영역에 문자열을 복사하는 것이므로, getCiphertext()에서는 필요없는 복사 작업이 수행되는 것이다. 따라서 첫 번째의 malloc()
코드를 선택했다.malloc()
과 strdup()
모두 사용 후에는 free()
를 사용하여 메모리를 해제해주어 메모리 낭비를 없애야 한다.int main(int argc, char **argv)
{
if (checkArg(argc, argv) == false)
{
printf("usage: ./caesar key\n");
return 1;
}
const int key = atoi(argv[1]);
char *plaintext = get_string("plaintext: ");
char *ciphertext = getCiphertext(key, plaintext);
printf("ciphertext: %s", ciphertext);
free(ciphertext);
printf("\n");
return 0;
}