From 303192b198768f4550ba8c644c2de4e841180f0c Mon Sep 17 00:00:00 2001 From: imnyang Date: Thu, 25 Jun 2026 13:48:22 +0900 Subject: [PATCH] wow --- .gitignore | 115 ++++++++++++++++- Makefile | 8 ++ include/gerbera.h | 19 +++ scripts/build.bat | 10 ++ scripts/build.sh | 7 + src/cipher.c | 319 ++++++++++++++++++++++++++++++++++++++++++++-- src/decrypt.c | 291 ++++++++++++++++++++++++++++++++++++++++-- src/encrypt.c | 265 +++++++++++++++++++++++++++++++++++--- src/main.c | 188 +++++++++++++++++++++++++-- 9 files changed, 1171 insertions(+), 51 deletions(-) diff --git a/.gitignore b/.gitignore index 15df4b0..199c7d9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,117 @@ tests/* !test/test.sh build/ .vscode -.DS_Store \ No newline at end of file + +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +# debug information files +*.dwo + +# General +.DS_Store +.localized +__MACOSX/ +.AppleDouble +.LSOverride +Icon[] + +# Resource forks +._* + +# Files and directories that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent +.com.apple.timemachine.supported +.PKInstallSandboxManager +.PKInstallSandboxManager-SystemSoftware +.hotfiles.btree +.vol +.file +.disk_label* +lost+found +.HFS+ Private Directory Data[] + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Mac OS 6 to 9 +Desktop DB +Desktop DF +TheFindByContentFolder +TheVolumeSettingsFolder +.FBCIndex +.FBCSemaphoreFile +.FBCLockFolder + +# Quota system +.quota.group +.quota.user +.quota.ops.group +.quota.ops.user + +# TimeMachine +Backups.backupdb +.MobileBackups +.MobileBackups.trash +MobileBackups.trash +tmbootpicker.efi \ No newline at end of file diff --git a/Makefile b/Makefile index 57afa8c..4c4c264 100644 --- a/Makefile +++ b/Makefile @@ -1,29 +1,37 @@ +# 컴파일러와 플래그 기본값은 환경 변수나 make 인자로 덮어쓸 수 있습니다. CC ?= cc CPPFLAGS ?= -Iinclude CFLAGS ?= -std=c11 -O2 -Wall -Wextra -Wpedantic LDLIBS ?= +# Windows 빌드는 호환 컴파일러/링커를 사용할 때 bcrypt가 필요합니다. ifeq ($(OS),Windows_NT) LDLIBS += -lbcrypt endif +# 최종 실행 파일 경로, 소스 목록, 여기서 파생되는 오브젝트 파일 경로입니다. TARGET := build/gerbera SOURCES := src/main.c src/encrypt.c src/decrypt.c src/cipher.c OBJECTS := $(SOURCES:src/%.c=build/%.o) +# 모든 오브젝트 파일을 컴파일한 뒤 실행 파일을 링크합니다. $(TARGET): $(OBJECTS) $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) +# 각 C 파일을 build/.o로 컴파일하며, 필요하면 build/를 먼저 만듭니다. build/%.o: src/%.c include/gerbera.h | build $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $< +# 컴파일이나 링크 전에 출력 디렉터리가 존재하도록 보장합니다. build: mkdir -p build .PHONY: clean test +# 방금 빌드한 실행 파일로 셸 테스트 모음을 실행합니다. test: $(TARGET) GERBERA_BIN=./$(TARGET) sh tests/test.sh +# 생성된 모든 빌드 산출물을 삭제합니다. clean: rm -rf build diff --git a/include/gerbera.h b/include/gerbera.h index 98914f8..4fb46ea 100644 --- a/include/gerbera.h +++ b/include/gerbera.h @@ -1,16 +1,23 @@ #ifndef GERBERA_H #define GERBERA_H +/* 명령줄 진입점과 각 모듈이 함께 쓰는 공개 선언입니다. */ #include #include +/* Gerbera 파일 형식과 메모리 작업 버퍼에서 사용하는 고정 크기입니다. */ #define GERBERA_MAGIC_SIZE 10 #define GERBERA_NONCE_SIZE 16 #define GERBERA_TAG_SIZE 16 #define GERBERA_BUFFER_SIZE 4096 + +/* 헤더에는 매직 바이트, nonce, 원문 길이가 저장됩니다. */ #define GERBERA_HEADER_SIZE (GERBERA_MAGIC_SIZE + GERBERA_NONCE_SIZE + 8) + +/* 암호화 파일은 데이터 앞에 헤더를, 뒤에 인증 태그를 붙입니다. */ #define GERBERA_OVERHEAD_SIZE (GERBERA_HEADER_SIZE + GERBERA_TAG_SIZE) +/* 스트림 암호 상태: 순열 테이블, 파생 키, 인덱스, 바이트 위치입니다. */ typedef struct { uint8_t state[256]; uint8_t key[32]; @@ -19,22 +26,34 @@ typedef struct { uint64_t position; } GerberaCipher; +/* 고정 크기 인증 태그를 계산하고 검증하는 상태입니다. */ typedef struct { uint8_t state[32]; uint64_t length; } GerberaAuth; +/* 비밀번호와 파일별 nonce로 암호 스트림을 초기화합니다. */ void gerbera_cipher_init(GerberaCipher *ctx, const char *password, const uint8_t nonce[GERBERA_NONCE_SIZE]); + +/* 생성된 키스트림을 data에 XOR합니다. 다시 호출하면 스트림이 이어집니다. */ void gerbera_cipher_apply(GerberaCipher *ctx, uint8_t data[], size_t length); +/* 같은 비밀번호와 nonce로 인증 누산기를 초기화합니다. */ void gerbera_auth_init(GerberaAuth *ctx, const char *password, const uint8_t nonce[GERBERA_NONCE_SIZE]); + +/* 파일에 기록되는 순서대로 바이트를 인증 누산기에 넣습니다. */ void gerbera_auth_update(GerberaAuth *ctx, const uint8_t data[], size_t length); + +/* 누산을 마무리하고 인증 태그를 씁니다. */ void gerbera_auth_finish(GerberaAuth *ctx, uint8_t tag[GERBERA_TAG_SIZE]); + +/* 첫 차이에서 바로 끝내지 않고 태그 전체를 비교합니다. */ int gerbera_constant_time_equal(const uint8_t a[], const uint8_t b[], size_t length); +/* 파일 전체를 암호화하거나 복호화하며, 성공 시 0, 오류 시 0이 아닌 값을 반환합니다. */ int encrypt_file(const char *input_path, const char *output_path, const char *password); int decrypt_file(const char *input_path, const char *output_path, diff --git a/scripts/build.bat b/scripts/build.bat index f588aea..2d4a84f 100644 --- a/scripts/build.bat +++ b/scripts/build.bat @@ -1,21 +1,28 @@ @echo off +rem 환경 변경이 이 스크립트 실행 안에만 머물도록 합니다. setlocal +rem Makefile과 POSIX 스크립트처럼 실행 파일을 build\에 둡니다. if not exist build mkdir build +rem Visual Studio 컴파일러가 있으면 우선 사용합니다. where cl >nul 2>nul if %errorlevel% equ 0 goto build_msvc +rem GCC가 설치되어 있고 PATH에서 보이면 다음 후보로 사용합니다. where gcc >nul 2>nul if %errorlevel% equ 0 goto build_gcc +rem 세 번째 지원 Windows 컴파일러로 Clang을 사용합니다. where clang >nul 2>nul if %errorlevel% equ 0 goto build_clang +rem 지원되는 컴파일러를 찾지 못했으므로 명확한 메시지를 출력하고 중단합니다. echo C compiler not found. Install Visual Studio Build Tools, GCC, or Clang. exit /b 1 :build_msvc +rem MSVC는 build\ 안에 gerbera.exe를 쓰고 bcrypt.lib를 명시적으로 링크합니다. pushd build cl /nologo /std:c11 /O2 /W4 /I..\include ^ ..\src\main.c ..\src\encrypt.c ..\src\decrypt.c ..\src\cipher.c ^ @@ -26,6 +33,7 @@ if not %result% equ 0 exit /b %result% goto success :build_gcc +rem GCC는 POSIX 빌드와 같은 경고 옵션을 쓰고 bcrypt를 링크합니다. gcc -Iinclude -std=c11 -O2 -Wall -Wextra -Wpedantic ^ src\main.c src\encrypt.c src\decrypt.c src\cipher.c ^ -o build\gerbera.exe -lbcrypt @@ -33,11 +41,13 @@ if not %errorlevel% equ 0 exit /b %errorlevel% goto success :build_clang +rem Clang은 이 작은 프로젝트에서 GCC 방식 명령줄을 그대로 따릅니다. clang -Iinclude -std=c11 -O2 -Wall -Wextra -Wpedantic ^ src\main.c src\encrypt.c src\decrypt.c src\cipher.c ^ -o build\gerbera.exe -lbcrypt if not %errorlevel% equ 0 exit /b %errorlevel% :success +rem 모든 컴파일러 선택지가 공유하는 성공 경로입니다. echo Build complete: build\gerbera.exe exit /b 0 diff --git a/scripts/build.sh b/scripts/build.sh index dcf37eb..77ba0ab 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,9 +1,15 @@ #!/bin/sh + +# 실패한 명령이나 정의되지 않은 변수에서 즉시 종료합니다. set -eu +# 호출자가 CC=...로 컴파일러를 고를 수 있고, 기본값은 cc입니다. compiler="${CC:-cc}" +# 컴파일 산출물은 소스 트리 밖의 build/에 둡니다. mkdir -p build + +# 모든 C 번역 단위를 묶어 Gerbera 명령 하나를 빌드합니다. "$compiler" \ -Iinclude \ -std=c11 \ @@ -17,4 +23,5 @@ mkdir -p build src/cipher.c \ -o build/gerbera +# 실행 파일이 어디에 만들어졌는지 알려 줍니다. echo "Build complete: build/gerbera" diff --git a/src/cipher.c b/src/cipher.c index f8ed3c4..83a7cf1 100644 --- a/src/cipher.c +++ b/src/cipher.c @@ -1,145 +1,446 @@ -#include "gerbera.h" +#include "gerbera.h" /* GerberaCipher, GerberaAuth, 상수 크기 등을 정의한 헤더 */ -#include +#include /* strlen(), memcpy() 사용 */ +/* uint8_t는 8비트 정수이므로, 이 함수는 1바이트 값을 왼쪽으로 회전합니다. */ static uint8_t rotate_left8(uint8_t value, unsigned count) { + /* count가 8 이상이면 8비트 회전에 맞게 0~7 범위로 줄입니다. */ count &= 7U; + + /* + * 왼쪽으로 count만큼 민 비트와, + * 오른쪽으로 8-count만큼 민 비트를 합쳐서 회전 효과를 만듭니다. + * + * ((8U - count) & 7U)를 쓰는 이유: + * count가 0일 때 value >> 8이 되면 C에서 미정의 동작이므로, + * 8을 다시 0으로 바꿔 안전하게 만듭니다. + */ return (uint8_t)((value << count) | (value >> ((8U - count) & 7U))); } /* - * Password bytes are folded into a 32-byte array many times. This is an - * educational, original construction; it is not a replacement for a - * reviewed password KDF such as Argon2. + * 비밀번호와 nonce를 이용해 32바이트 키를 만듭니다. + * + * key : 결과로 채워질 32바이트 키 + * password : 사용자가 입력한 비밀번호 + * nonce : 파일마다 달라지는 랜덤값 + * domain : 암호화용 키와 인증용 키를 서로 다르게 만들기 위한 구분값 */ static void derive_key(uint8_t key[32], const char *password, const uint8_t nonce[GERBERA_NONCE_SIZE], uint8_t domain) { + /* + * 키 파생의 시작점으로 사용할 고정된 32바이트 값입니다. + * 전부 0에서 시작하면 섞이는 정도가 약해질 수 있으므로, + * 0이 아닌 값들로 시작합니다. + */ static const uint8_t initial[32] = { - 0x41, 0x6b, 0x79, 0x61, 0x6d, 0x61, 0x4d, 0x69, + 0x41, 0x6b, 0x79, 0x61, 0x6d, 0x61, 0x4d, 0x69, 0x7a, 0x75, 0x6b, 0x69, 0x4e, 0x41, 0x45, 0x31, 0x9d, 0x37, 0x6b, 0xa1, 0xf4, 0x52, 0xc8, 0x0e, 0x73, 0xbf, 0x25, 0xda, 0x86, 0x19, 0xe0, 0x4c }; + + /* 비밀번호 길이를 미리 구해 반복문에서 사용합니다. */ size_t password_length = strlen(password); + + /* 키 파생을 여러 번 반복하기 위한 라운드 변수입니다. */ unsigned round; + + /* 비밀번호의 각 바이트를 순회하기 위한 인덱스입니다. */ size_t p; + /* key 배열을 initial 값으로 초기화합니다. */ memcpy(key, initial, sizeof(initial)); + + /* + * domain 값을 key[0]에 섞습니다. + * 이렇게 하면 같은 password와 nonce를 써도, + * 암호화용 키와 인증용 키가 서로 달라집니다. + */ key[0] ^= domain; + /* + * 비밀번호와 nonce를 8192번 반복해서 섞습니다. + * 반복 횟수가 많을수록 단순한 비밀번호를 바로 키로 쓰는 것보다 느려져서, + * 무차별 대입 공격 비용을 조금 높일 수 있습니다. + */ for (round = 0; round < 8192U; ++round) { + /* 비밀번호의 모든 바이트를 하나씩 키 상태에 섞습니다. */ for (p = 0; p < password_length; ++p) { + /* + * a는 현재 갱신할 key 위치입니다. + * & 31U는 32로 나눈 나머지와 같아서 0~31 범위가 됩니다. + */ size_t a = (p + round) & 31U; + + /* b는 a에서 11칸 떨어진 위치입니다. */ size_t b = (a + 11U) & 31U; + + /* c는 a에서 23칸 떨어진 위치입니다. */ size_t c = (a + 23U) & 31U; + + /* + * mixed는 현재 키 바이트, 비밀번호 바이트, nonce 바이트, + * 라운드 번호를 모두 더해서 만든 중간값입니다. + * + * uint8_t로 캐스팅하므로 0~255 범위만 남습니다. + */ uint8_t mixed = (uint8_t)(key[b] + (uint8_t)password[p] + nonce[(p + round) & 15U] + (uint8_t)round); + + /* + * key[a]를 갱신합니다. + * + * 1. 기존 key[a]와 mixed를 XOR합니다. + * 2. key[c]의 하위 3비트 값을 회전 횟수로 사용합니다. + * 3. 회전한 값에 key[c]를 더합니다. + * + * 결과적으로 key[a]는 password, nonce, round, 다른 key 위치의 영향을 받습니다. + */ key[a] = (uint8_t)(rotate_left8((uint8_t)(key[a] ^ mixed), (unsigned)(key[c] & 7U)) + key[c]); } + + /* + * 비밀번호가 매우 짧거나 비어 있더라도, + * 매 라운드마다 key 내부 값끼리 한 번 더 섞이도록 합니다. + */ key[round & 31U] ^= rotate_left8(key[(round + 17U) & 31U], round & 7U); } } +/* + * 스트림 암호 상태를 초기화합니다. + * + * ctx : 암호화 상태 구조체 + * password : 비밀번호 + * nonce : 파일마다 다른 nonce + */ void gerbera_cipher_init(GerberaCipher *ctx, const char *password, - const uint8_t nonce[GERBERA_NONCE_SIZE]) + const uint8_t nonce[GERBERA_NONCE_SIZE]) { + /* 0~255 순열 테이블을 초기화할 때 쓰는 인덱스입니다. */ unsigned n; + + /* RC4 계열 알고리즘에서 사용하는 두 번째 인덱스 역할입니다. */ uint8_t j = 0; + /* + * 암호화용 키를 파생합니다. + * domain 값 0x43은 cipher의 C라고 생각하면 됩니다. + */ derive_key(ctx->key, password, nonce, 0x43U); + + /* + * state 배열을 0, 1, 2, ..., 255로 채웁니다. + * 이 배열은 이후 키에 따라 섞이는 순열 테이블입니다. + */ for (n = 0; n < 256U; ++n) { ctx->state[n] = (uint8_t)n; } + + /* + * 파생된 key와 nonce를 이용해 state 배열을 섞습니다. + * 256번이 아니라 1024번 반복해서 같은 테이블을 여러 차례 뒤섞습니다. + */ for (n = 0; n < 1024U; ++n) { + /* + * i는 현재 섞을 state 위치입니다. + * n이 256 이상이어도 uint8_t로 줄어들어 0~255를 반복합니다. + */ uint8_t i = (uint8_t)n; + + /* state[i]와 state[j]를 교환할 때 임시로 저장하는 변수입니다. */ uint8_t temporary; + + /* + * j를 이동시킵니다. + * + * 현재 j, state[i], key 일부, nonce 일부가 모두 j 계산에 들어갑니다. + * uint8_t라서 255를 넘으면 자동으로 0~255 범위로 wrap 됩니다. + */ j = (uint8_t)(j + ctx->state[i] + ctx->key[n & 31U] + nonce[n & 15U]); + + /* state[i] 값을 임시 변수에 저장합니다. */ temporary = ctx->state[i]; + + /* state[j] 값을 state[i] 위치로 옮깁니다. */ ctx->state[i] = ctx->state[j]; + + /* 임시로 저장했던 원래 state[i] 값을 state[j]에 넣습니다. */ ctx->state[j] = temporary; } + + /* 키스트림 생성에 사용할 i 인덱스를 0으로 초기화합니다. */ ctx->i = 0; + + /* + * j는 섞기 과정이 끝난 뒤의 값을 그대로 사용합니다. + * 이렇게 하면 초기 순열 상태와 j 상태가 같이 맞춰집니다. + */ ctx->j = j; + + /* 지금까지 처리한 바이트 위치를 0으로 초기화합니다. */ ctx->position = 0; } +/* + * data 배열에 스트림 암호를 적용합니다. + * + * XOR 기반이므로 같은 함수로 암호화와 복호화를 모두 수행할 수 있습니다. + */ void gerbera_cipher_apply(GerberaCipher *ctx, uint8_t data[], size_t length) { + /* data의 각 바이트를 순회하기 위한 인덱스입니다. */ size_t n; + /* 입력 데이터 길이만큼 한 바이트씩 처리합니다. */ for (n = 0; n < length; ++n) { + /* state 값을 교환할 때 사용할 임시 변수입니다. */ uint8_t temporary; + + /* 이번 위치에서 생성된 키스트림 바이트입니다. */ uint8_t stream; + + /* i를 한 칸 전진시킵니다. */ ctx->i = (uint8_t)(ctx->i + 1U); + + /* + * j도 전진시킵니다. + * + * 현재 state[i] 값과 key의 현재 위치 값을 함께 섞어 + * 키스트림이 위치에 따라 달라지도록 합니다. + */ ctx->j = (uint8_t)(ctx->j + ctx->state[ctx->i] + ctx->key[ctx->position & 31U]); + + /* state[i]를 임시 변수에 저장합니다. */ temporary = ctx->state[ctx->i]; + + /* state[j] 값을 state[i] 위치로 옮깁니다. */ ctx->state[ctx->i] = ctx->state[ctx->j]; + + /* 원래 state[i] 값을 state[j]에 넣어 두 값을 교환합니다. */ ctx->state[ctx->j] = temporary; + + /* + * 교환 이후의 state[i]와 state[j] 값을 더해서 + * state 배열에서 키스트림 후보 바이트 하나를 가져옵니다. + */ stream = ctx->state[(uint8_t)(ctx->state[ctx->i] + ctx->state[ctx->j])]; + + /* + * 키스트림에 key 값을 한 번 더 XOR합니다. + * + * (ctx->position + stream) & 31U: + * 현재 위치와 stream 값에 따라 key의 0~31 위치 중 하나를 고릅니다. + * + * ctx->position & 7U: + * 현재 위치에 따라 0~7만큼 회전합니다. + */ stream ^= rotate_left8(ctx->key[(ctx->position + stream) & 31U], (unsigned)(ctx->position & 7U)); + + /* + * 원본 데이터 바이트와 키스트림 바이트를 XOR합니다. + * + * 암호화: + * 평문 ^ 키스트림 = 암호문 + * + * 복호화: + * 암호문 ^ 같은 키스트림 = 평문 + */ data[n] ^= stream; + + /* 처리한 바이트 수를 1 증가시킵니다. */ ++ctx->position; } } +/* + * 인증 태그를 만들기 위한 상태를 초기화합니다. + * + * 암호화와 별개로 데이터 무결성 확인용 태그를 만들 때 사용합니다. + */ void gerbera_auth_init(GerberaAuth *ctx, const char *password, - const uint8_t nonce[GERBERA_NONCE_SIZE]) + const uint8_t nonce[GERBERA_NONCE_SIZE]) { + /* + * 인증용 state를 파생합니다. + * domain 값 0x41은 auth의 A라고 생각하면 됩니다. + * + * 암호화용 domain과 다르기 때문에, + * 같은 password와 nonce를 써도 인증 상태는 암호 상태와 달라집니다. + */ derive_key(ctx->state, password, nonce, 0x41U); + + /* 아직 처리한 데이터가 없으므로 길이는 0입니다. */ ctx->length = 0; } +/* + * 인증 상태에 데이터를 흡수합니다. + * + * data의 모든 바이트가 ctx->state에 누적되어, + * 최종적으로 tag 생성에 영향을 줍니다. + */ void gerbera_auth_update(GerberaAuth *ctx, const uint8_t data[], size_t length) { + /* data 배열을 순회할 인덱스입니다. */ size_t n; + /* 입력 데이터를 한 바이트씩 처리합니다. */ for (n = 0; n < length; ++n) { + /* + * a는 이번 바이트가 주로 반영될 state 위치입니다. + * 전체 처리 길이를 기준으로 0~31 위치를 순환합니다. + */ size_t a = (size_t)(ctx->length & 31U); + + /* b는 a에서 7칸 떨어진 위치입니다. */ size_t b = (a + 7U) & 31U; + + /* c는 a에서 19칸 떨어진 위치입니다. */ size_t c = (a + 19U) & 31U; + + /* + * state[a]를 갱신합니다. + * + * 1. 기존 state[a] + * 2. 현재 입력 바이트 data[n] + * 3. 다른 위치의 state[c] + * + * 이 셋을 XOR한 뒤, + * state[b]의 하위 3비트를 회전 횟수로 사용해 회전합니다. + * 이후 state[b]와 a 값을 더해 최종적으로 state[a]에 넣습니다. + */ ctx->state[a] = (uint8_t)(rotate_left8( (uint8_t)(ctx->state[a] ^ data[n] ^ ctx->state[c]), (unsigned)(ctx->state[b] & 7U)) + ctx->state[b] + (uint8_t)a); + + /* + * 방금 갱신된 state[a]와 현재 입력 바이트를 이용해 + * state[c]도 다시 갱신합니다. + * + * 이렇게 하면 한 입력 바이트가 state 한 곳에만 머물지 않고, + * 다른 위치에도 영향을 주게 됩니다. + */ ctx->state[c] ^= rotate_left8((uint8_t)(data[n] + ctx->state[a]), (unsigned)(a & 7U)); + + /* 전체 처리 길이를 1 증가시킵니다. */ ++ctx->length; } } +/* + * 지금까지 auth_update로 흡수한 데이터에서 최종 인증 태그를 만듭니다. + */ void gerbera_auth_finish(GerberaAuth *ctx, uint8_t tag[GERBERA_TAG_SIZE]) { + /* + * 메시지 길이를 8바이트 little-endian 형태로 저장할 배열입니다. + * 길이를 태그에 포함하면, 뒤에 0바이트가 붙은 메시지와 구분할 수 있습니다. + */ uint8_t trailer[8]; + + /* 반복문에서 사용할 인덱스입니다. */ unsigned n; + /* + * ctx->length를 8바이트로 나누어 trailer에 저장합니다. + * + * n = 0일 때 하위 8비트, + * n = 1일 때 그 다음 8비트, + * 이런 식으로 little-endian 순서가 됩니다. + */ for (n = 0; n < 8U; ++n) { trailer[n] = (uint8_t)(ctx->length >> (n * 8U)); } + + /* + * 메시지 길이 정보도 인증 상태에 흡수합니다. + * 이렇게 하면 같은 내용이라도 길이 처리가 명확해집니다. + */ gerbera_auth_update(ctx, trailer, sizeof(trailer)); + + /* + * 최종 확산 단계입니다. + * + * 256번 반복하면서 state 내부의 서로 다른 위치들을 다시 섞습니다. + * 32바이트 상태를 8바퀴 도는 효과가 있습니다. + */ for (n = 0; n < 256U; ++n) { + /* + * 현재 n 기준으로 이웃한 두 state 값을 가져와 더하고, + * n 자체도 더해서 중간값을 만듭니다. + */ uint8_t value = (uint8_t)(ctx->state[(n + 1U) & 31U] + ctx->state[(n + 13U) & 31U] + n); + + /* + * value를 n에 따라 0~7비트 회전한 뒤, + * 현재 state 위치에 XOR하여 최종 상태를 더 섞습니다. + */ ctx->state[n & 31U] ^= rotate_left8(value, n & 7U); } + + /* + * 내부 state는 32바이트지만, + * 공개할 인증 태그는 GERBERA_TAG_SIZE 바이트입니다. + * + * 보통 여기서는 16바이트 태그를 만든다고 가정합니다. + */ for (n = 0; n < GERBERA_TAG_SIZE; ++n) { + /* + * state 앞쪽 16바이트와 뒤쪽 16바이트를 XOR해서 + * 최종 tag 바이트로 압축합니다. + */ tag[n] = (uint8_t)(ctx->state[n] ^ ctx->state[n + 16U]); } } +/* + * 두 바이트 배열이 같은지 비교합니다. + * + * 일반 memcmp()는 첫 번째로 다른 위치에서 바로 멈출 수 있어, + * 실행 시간 차이로 어느 위치가 다른지 추측될 수 있습니다. + * + * 이 함수는 항상 length 전체를 끝까지 비교합니다. + */ int gerbera_constant_time_equal(const uint8_t a[], const uint8_t b[], size_t length) { + /* 배열을 순회할 인덱스입니다. */ size_t n; + + /* + * 지금까지 발견한 차이를 누적하는 변수입니다. + * 하나라도 다른 바이트가 있으면 0이 아닌 값이 됩니다. + */ uint8_t difference = 0; + /* + * 모든 바이트를 끝까지 비교합니다. + * 중간에 다르다고 바로 return하지 않습니다. + */ for (n = 0; n < length; ++n) { + /* + * a[n]과 b[n]이 같으면 XOR 결과는 0입니다. + * 다르면 0이 아닌 값입니다. + * + * OR로 누적하므로, 한 번이라도 차이가 있으면 difference는 0이 아니게 됩니다. + */ difference |= (uint8_t)(a[n] ^ b[n]); } + + /* + * difference가 0이면 모든 바이트가 같다는 뜻입니다. + * 같으면 1, 다르면 0을 반환합니다. + */ return difference == 0; -} +} \ No newline at end of file diff --git a/src/decrypt.c b/src/decrypt.c index a81a917..7ec5afa 100644 --- a/src/decrypt.c +++ b/src/decrypt.c @@ -1,111 +1,376 @@ -#include "gerbera.h" +#include "gerbera.h" /* Gerbera 관련 타입, 상수, 함수 선언을 가져옵니다. */ -#include -#include +#include /* FILE, fopen(), fread(), fwrite(), fprintf() 사용 */ +#include /* memcmp() 사용 */ +/* + * Gerbera 암호문인지 확인하기 위한 고정 파일 서명입니다. + * encrypt_file에서 쓴 magic 값과 반드시 같아야 합니다. + */ static const uint8_t expected_magic[GERBERA_MAGIC_SIZE] = {'M', 'I', 'Z', 'U', 'K', 'I', 0xF0, 0x9F, 0x8E, 0x80}; +/* + * little-endian 순서로 저장된 8바이트를 uint64_t 값으로 복원합니다. + * + * bytes: + * 파일 헤더에서 읽은 8바이트 원문 크기 필드 + */ static uint64_t decode_u64_le(const uint8_t bytes[8]) { + /* 최종적으로 조립될 64비트 정수입니다. */ uint64_t value = 0; + + /* 0번 바이트부터 7번 바이트까지 순회하기 위한 변수입니다. */ unsigned n; + + /* 낮은 바이트부터 높은 바이트까지 순서대로 조립합니다. */ for (n = 0; n < 8U; ++n) { + /* + * bytes[n]을 uint64_t로 확장한 뒤, + * 원래 위치인 n * 8비트만큼 왼쪽으로 이동시킵니다. + * + * OR 연산으로 value에 누적하면 8바이트가 하나의 64비트 값이 됩니다. + */ value |= (uint64_t)bytes[n] << (n * 8U); } + + /* 조립된 64비트 값을 반환합니다. */ return value; } +/* + * input_path에 있는 Gerbera 암호문 파일을 복호화해서 output_path에 저장합니다. + * + * 반환값: + * 0 = 성공 + * 1 = 실패 + */ int decrypt_file(const char *input_path, const char *output_path, const char *password) { + /* 암호문 입력 파일 포인터입니다. 아직 열지 않았으므로 NULL로 초기화합니다. */ FILE *input = NULL; + + /* 복호화 결과를 쓸 출력 파일 포인터입니다. */ FILE *output = NULL; + + /* 파일에서 읽은 magic 값을 저장합니다. */ uint8_t magic[GERBERA_MAGIC_SIZE]; + + /* 파일에서 읽은 nonce를 저장합니다. */ uint8_t nonce[GERBERA_NONCE_SIZE]; + + /* 파일에서 읽은 원문 크기 필드 8바이트를 저장합니다. */ uint8_t size_bytes[8]; + + /* 암호문을 청크 단위로 읽고 복호화할 임시 버퍼입니다. */ uint8_t buffer[GERBERA_BUFFER_SIZE]; + + /* 파일 끝에 저장되어 있던 인증 태그입니다. */ uint8_t stored_tag[GERBERA_TAG_SIZE]; + + /* 현재 입력 파일 내용으로 다시 계산한 인증 태그입니다. */ uint8_t calculated_tag[GERBERA_TAG_SIZE]; + + /* 복호화에 사용할 스트림 암호 상태입니다. */ GerberaCipher cipher; + + /* 인증 태그 검증에 사용할 인증 상태입니다. */ GerberaAuth auth; + + /* 헤더에 기록된 원래 평문 크기입니다. */ uint64_t plain_size; + + /* 아직 처리해야 할 암호문 본문 바이트 수입니다. */ uint64_t remaining; + + /* 전체 암호문 파일 크기입니다. ftell()이 long을 반환하므로 long을 사용합니다. */ long file_size; + + /* fread()로 실제 읽은 바이트 수입니다. */ size_t count; + + /* + * 함수 결과입니다. + * 기본값을 실패인 1로 두고, 모든 과정이 성공하면 0으로 바꿉니다. + */ int result = 1; + /* + * 입력 암호문 파일을 바이너리 읽기 모드로 엽니다. + * "rb"는 줄바꿈 변환 없이 파일 바이트를 그대로 읽습니다. + */ input = fopen(input_path, "rb"); + + /* + * 파일을 열고, + * 파일 끝으로 이동해서 크기를 구한 뒤, + * 다시 처음으로 되돌립니다. + * + * 하나라도 실패하면 복호화를 진행할 수 없습니다. + */ if (input == NULL || fseek(input, 0, SEEK_END) != 0 || (file_size = ftell(input)) < 0 || fseek(input, 0, SEEK_SET) != 0) { + /* 입력 파일을 열 수 없거나 크기를 구할 수 없다는 메시지입니다. */ fprintf(stderr, "Could not open the encrypted file.\n"); + + /* 열린 파일이 있으면 닫기 위해 cleanup으로 이동합니다. */ goto cleanup; } + + /* + * 파일이 최소한의 Gerbera 파일 크기보다 작은지 확인하고, + * 헤더의 magic, nonce, size_bytes를 차례대로 읽습니다. + * + * 또한 magic이 expected_magic과 같은지 확인합니다. + */ if (file_size < GERBERA_OVERHEAD_SIZE || fread(magic, 1, sizeof(magic), input) != sizeof(magic) || fread(nonce, 1, sizeof(nonce), input) != sizeof(nonce) || fread(size_bytes, 1, sizeof(size_bytes), input) != sizeof(size_bytes) || memcmp(magic, expected_magic, sizeof(magic)) != 0) { + /* + * 파일 구조가 너무 작거나, + * 헤더를 제대로 읽지 못했거나, + * magic 값이 다르면 Gerbera 파일이 아니라고 처리합니다. + */ fprintf(stderr, "The input is not a Gerbera file.\n"); + + /* cleanup으로 이동합니다. */ goto cleanup; } + + /* + * 헤더의 8바이트 원문 크기 필드를 uint64_t로 복원합니다. + */ plain_size = decode_u64_le(size_bytes); + + /* + * Gerbera 파일 구조는 다음과 같습니다. + * + * header + encrypted_body + tag + * + * encrypted_body의 길이는 원문 길이와 같아야 합니다. + * 스트림 암호는 XOR 방식이라 암호화 전후 길이가 변하지 않기 때문입니다. + */ if (plain_size != (uint64_t)(file_size - GERBERA_OVERHEAD_SIZE)) { + /* 헤더에 기록된 크기와 실제 파일 크기가 맞지 않는 경우입니다. */ fprintf(stderr, "The file size metadata is invalid.\n"); + + /* cleanup으로 이동합니다. */ goto cleanup; } - /* First pass: authenticate before any plaintext is written. */ + /* + * 첫 번째 통과에서는 복호화를 하지 않고 인증 태그를 먼저 검증합니다. + * 인증에 실패하면 출력 파일을 만들지 않기 위함입니다. + */ gerbera_auth_init(&auth, password, nonce); + + /* + * magic 값을 인증 상태에 넣습니다. + * 암호화 때도 같은 순서로 인증했으므로 복호화 때도 순서가 같아야 합니다. + */ gerbera_auth_update(&auth, magic, sizeof(magic)); + + /* + * nonce 값을 인증 상태에 넣습니다. + * nonce가 바뀌면 키스트림이 바뀌므로 반드시 검증 대상이어야 합니다. + */ gerbera_auth_update(&auth, nonce, sizeof(nonce)); + + /* + * 원문 크기 필드도 인증 상태에 넣습니다. + * size_bytes가 변조되면 태그 검증에 실패해야 합니다. + */ gerbera_auth_update(&auth, size_bytes, sizeof(size_bytes)); + + /* + * remaining은 앞으로 인증해야 할 암호문 본문 길이입니다. + * 처음에는 전체 원문 크기와 같습니다. + */ remaining = plain_size; + + /* + * 암호문 본문을 끝까지 읽으면서 인증 상태에 넣습니다. + * 여기서는 아직 복호화하지 않습니다. + */ while (remaining > 0) { + /* + * 이번에 읽을 바이트 수입니다. + * + * 남은 크기가 buffer보다 작으면 남은 크기만큼, + * 아니면 buffer 전체 크기만큼 읽습니다. + */ size_t wanted = remaining < sizeof(buffer) ? (size_t)remaining : sizeof(buffer); + + /* + * 암호문 본문에서 wanted 바이트만큼 읽습니다. + */ count = fread(buffer, 1, wanted, input); + + /* + * 정확히 wanted만큼 읽지 못했다면 파일이 중간에 잘렸거나 읽기 오류가 난 것입니다. + */ if (count != wanted) { + /* 암호문 데이터를 읽지 못했다는 메시지입니다. */ fprintf(stderr, "Could not read the encrypted data.\n"); + + /* cleanup으로 이동합니다. */ goto cleanup; } + + /* + * 읽은 암호문 바이트를 인증 상태에 넣습니다. + * 암호화할 때도 암호문 기준으로 태그를 만들었으므로 그대로 맞춥니다. + */ gerbera_auth_update(&auth, buffer, count); + + /* 처리한 만큼 남은 바이트 수를 줄입니다. */ remaining -= count; } + + /* + * 암호문 본문 바로 뒤에는 저장된 인증 태그가 있어야 합니다. + */ if (fread(stored_tag, 1, sizeof(stored_tag), input) != sizeof(stored_tag)) { + /* 태그를 읽지 못하면 파일이 잘렸거나 형식이 깨진 것입니다. */ fprintf(stderr, "The authentication tag is missing.\n"); - goto cleanup; - } - gerbera_auth_finish(&auth, calculated_tag); - if (!gerbera_constant_time_equal(stored_tag, calculated_tag, sizeof(stored_tag))) { - fprintf(stderr, "The password is incorrect or the file is corrupted.\n"); + + /* cleanup으로 이동합니다. */ goto cleanup; } - output = fopen(output_path, "wb"); - if (output == NULL || fseek(input, GERBERA_HEADER_SIZE, SEEK_SET) != 0) { - fprintf(stderr, "Could not create the output file.\n"); + /* + * 지금까지 읽은 헤더와 암호문으로 인증 태그를 다시 계산합니다. + */ + gerbera_auth_finish(&auth, calculated_tag); + + /* + * 파일에 저장된 태그와 방금 계산한 태그를 비교합니다. + * + * gerbera_constant_time_equal()을 사용해서 + * 어디서부터 다른지 시간 차이로 드러나지 않게 합니다. + */ + if (!gerbera_constant_time_equal(stored_tag, calculated_tag, sizeof(stored_tag))) { + /* + * 태그가 다르면 비밀번호가 틀렸거나, + * 파일 내용이 변조되었거나, + * 파일이 손상된 것입니다. + */ + fprintf(stderr, "The password is incorrect or the file is corrupted.\n"); + + /* 인증 실패 시 출력 파일을 만들지 않고 종료합니다. */ goto cleanup; } + + /* + * 인증에 성공했으므로 이제 출력 파일을 만듭니다. + * "wb"는 바이너리 쓰기 모드이며 기존 파일이 있으면 덮어씁니다. + */ + output = fopen(output_path, "wb"); + + /* + * 입력 파일 위치를 암호문 본문 시작 위치로 되돌립니다. + * 헤더 크기만큼 건너뛴 위치입니다. + */ + if (output == NULL || fseek(input, GERBERA_HEADER_SIZE, SEEK_SET) != 0) { + /* + * 출력 파일 생성 실패 또는 입력 위치 이동 실패입니다. + * + * 메시지는 출력 파일 생성 실패처럼 되어 있지만, + * 실제로는 fseek 실패도 포함합니다. + */ + fprintf(stderr, "Could not create the output file.\n"); + + /* cleanup으로 이동합니다. */ + goto cleanup; + } + + /* + * 암호화 때 사용한 것과 같은 password와 nonce로 + * 스트림 암호 상태를 다시 초기화합니다. + * + * 같은 키스트림이 생성되어야 XOR 복호화가 가능합니다. + */ gerbera_cipher_init(&cipher, password, nonce); + + /* + * 다시 암호문 본문 전체 길이만큼 복호화해야 하므로 + * remaining을 plain_size로 되돌립니다. + */ remaining = plain_size; + + /* + * 인증이 끝난 암호문 본문을 다시 읽으면서 복호화합니다. + */ while (remaining > 0) { + /* + * 이번에 읽을 바이트 수입니다. + * 마지막 청크에서는 buffer보다 작을 수 있습니다. + */ size_t wanted = remaining < sizeof(buffer) ? (size_t)remaining : sizeof(buffer); + + /* + * 암호문 본문에서 wanted 바이트만큼 읽습니다. + */ count = fread(buffer, 1, wanted, input); + + /* + * 정확히 wanted만큼 읽지 못하면 파일 읽기 오류입니다. + * 다만 앞에서 인증을 통과했으므로 보통은 발생하지 않아야 합니다. + */ if (count != wanted) { + /* 복호화용 데이터를 읽지 못했다는 메시지입니다. */ fprintf(stderr, "Could not read data for decryption.\n"); + + /* cleanup으로 이동합니다. */ goto cleanup; } + + /* + * 스트림 암호를 적용해 암호문을 평문으로 되돌립니다. + * XOR 기반이라 암호화와 같은 함수가 복호화에도 사용됩니다. + */ gerbera_cipher_apply(&cipher, buffer, count); + + /* + * 복호화된 평문 청크를 출력 파일에 씁니다. + */ if (fwrite(buffer, 1, count, output) != count) { + /* 출력 파일 쓰기 실패 메시지입니다. */ fprintf(stderr, "Could not write the decrypted data.\n"); + + /* cleanup으로 이동합니다. */ goto cleanup; } + + /* 복호화한 만큼 남은 바이트 수를 줄입니다. */ remaining -= count; } + + /* + * 여기까지 왔다면 인증과 복호화, 출력 쓰기가 모두 성공한 것입니다. + */ result = 0; cleanup: + /* + * input이 열려 있다면 닫습니다. + * NULL이면 열리지 않았다는 뜻이므로 닫지 않습니다. + */ if (input != NULL) fclose(input); + + /* + * output이 열려 있다면 닫습니다. + * fclose() 실패는 출력 파일이 완전히 기록되지 않았을 수 있으므로 실패 처리합니다. + */ if (output != NULL && fclose(output) != 0) result = 1; + + /* + * 최종 결과를 반환합니다. + * 0이면 성공, 1이면 실패입니다. + */ return result; -} +} \ No newline at end of file diff --git a/src/encrypt.c b/src/encrypt.c index 90a0815..27a771c 100644 --- a/src/encrypt.c +++ b/src/encrypt.c @@ -1,103 +1,336 @@ -#include "gerbera.h" +#include "gerbera.h" /* Gerbera 관련 타입, 상수, 함수 선언을 가져옵니다. */ -#include -#include -#include +#include /* FILE, fopen(), fread(), fwrite(), fprintf() 사용 */ +#include /* srand(), rand(), remove() 사용 */ +#include /* time(), clock() 사용 */ +/* + * 암호화된 파일인지 확인하기 위한 고정 파일 서명입니다. + * 복호화할 때 파일 맨 앞이 이 값과 일치하는지 확인할 수 있습니다. + */ static const uint8_t magic[GERBERA_MAGIC_SIZE] = {'M', 'I', 'Z', 'U', 'K', 'I', 0xF0, 0x9F, 0x8E, 0x80}; +/* + * 64비트 정수를 little-endian 바이트 배열로 변환합니다. + * + * value: + * 저장할 64비트 정수 + * + * bytes: + * 결과가 저장될 8바이트 배열 + */ static void encode_u64_le(uint8_t bytes[8], uint64_t value) { + /* 0번 바이트부터 7번 바이트까지 순회하기 위한 변수입니다. */ unsigned n; + + /* 64비트 값을 8비트씩 잘라서 낮은 바이트부터 저장합니다. */ for (n = 0; n < 8U; ++n) { + /* + * n * 8비트만큼 오른쪽으로 밀면 원하는 바이트가 가장 아래로 옵니다. + * uint8_t로 캐스팅해서 하위 8비트만 저장합니다. + */ bytes[n] = (uint8_t)(value >> (n * 8U)); } } +/* + * nonce를 채웁니다. + * + * nonce는 같은 비밀번호와 같은 파일을 암호화해도 + * 매번 다른 암호문이 나오게 만드는 값입니다. + */ static int random_nonce(uint8_t nonce[GERBERA_NONCE_SIZE]) { + /* + * srand()가 이미 호출되었는지 저장하는 정적 변수입니다. + * static 지역 변수라서 함수 호출이 끝나도 값이 유지됩니다. + */ static int initialized = 0; + /* + * 난수 생성기 시드는 한 번만 설정합니다. + * 매번 srand()를 다시 호출하면 짧은 시간 안에 같은 시드가 들어갈 수 있습니다. + */ if (!initialized) { + /* + * time(NULL): + * 현재 시간을 초 단위로 반환합니다. + * + * clock(): + * 프로그램이 사용한 CPU 시간을 반환합니다. + * + * 두 값을 XOR해서 rand()의 시드로 사용합니다. + * + * 단, rand()는 보안용 난수가 아니므로 + * 실제 암호화 프로그램에서는 권장되지 않습니다. + */ srand((unsigned)time(NULL) ^ (unsigned)clock()); + + /* 이제 시드 설정이 끝났다고 표시합니다. */ initialized = 1; } + /* nonce 배열의 모든 바이트를 채웁니다. */ for (size_t i = 0; i < GERBERA_NONCE_SIZE; ++i) { + /* + * rand()가 반환한 정수의 하위 8비트만 사용합니다. + * 이 값이 nonce의 한 바이트가 됩니다. + */ nonce[i] = (uint8_t)rand(); } + /* + * 현재 구현에서는 실패할 수 있는 시스템 난수 API를 쓰지 않으므로 + * 항상 성공을 의미하는 1을 반환합니다. + */ return 1; } -int encrypt_file(const char *input_path, const char *output_path, - const char *password) +/* + * input_path에 있는 파일을 읽어서 암호화한 뒤, + * output_path에 Gerbera 암호문 형식으로 저장합니다. + * + * 반환값: + * 0 = 성공 + * 1 = 실패 + */ +int encrypt_file(const char *input_path, const char *output_path, const char *password) { + /* 입력 파일 포인터입니다. 아직 열지 않았으므로 NULL로 초기화합니다. */ FILE *input = NULL; + + /* 출력 파일 포인터입니다. 아직 열지 않았으므로 NULL로 초기화합니다. */ FILE *output = NULL; + + /* 파일마다 새로 생성되는 nonce입니다. */ uint8_t nonce[GERBERA_NONCE_SIZE]; + + /* 원본 파일 크기를 little-endian으로 저장할 8바이트 배열입니다. */ uint8_t size_bytes[8]; + + /* 파일을 청크 단위로 읽고 암호화할 임시 버퍼입니다. */ uint8_t buffer[GERBERA_BUFFER_SIZE]; + + /* 최종 인증 태그를 저장할 배열입니다. */ uint8_t tag[GERBERA_TAG_SIZE]; + + /* 스트림 암호 상태입니다. */ GerberaCipher cipher; + + /* 인증 태그 계산 상태입니다. */ GerberaAuth auth; + + /* 입력 파일 크기를 저장합니다. ftell()이 long을 반환하므로 long을 사용합니다. */ long input_size; + + /* fread()로 실제 읽은 바이트 수를 저장합니다. */ size_t count; + + /* + * 함수 결과입니다. + * 기본값을 실패인 1로 두고, + * 모든 과정이 성공했을 때만 0으로 바꿉니다. + */ int result = 1; + /* + * 입력 파일을 바이너리 읽기 모드로 엽니다. + * "rb"를 쓰면 Windows에서도 줄바꿈 변환 없이 원본 바이트 그대로 읽습니다. + */ input = fopen(input_path, "rb"); + + /* + * 입력 파일을 열었는지 확인하고, + * 파일 끝으로 이동한 뒤, + * ftell()로 파일 크기를 얻고, + * 다시 파일 처음으로 돌아옵니다. + * + * 하나라도 실패하면 암호화를 진행할 수 없습니다. + */ if (input == NULL || fseek(input, 0, SEEK_END) != 0 || (input_size = ftell(input)) < 0 || fseek(input, 0, SEEK_SET) != 0) { + /* + * 오류 메시지를 표준 에러로 출력합니다. + */ fprintf(stderr, "Could not open the input file or determine its size.\n"); - goto cleanup; - } - if (!random_nonce(nonce)) { - fprintf(stderr, "Could not generate a secure random nonce.\n"); - goto cleanup; - } - output = fopen(output_path, "wb"); - if (output == NULL) { - fprintf(stderr, "Could not create the output file.\n"); + + /* + * cleanup으로 이동해 열려 있을 수 있는 파일을 닫고 종료합니다. + */ goto cleanup; } + /* + * 암호화 파일마다 다른 nonce를 생성합니다. + * nonce가 달라지면 같은 비밀번호와 같은 평문이어도 암호문이 달라집니다. + */ + if (!random_nonce(nonce)) { + /* nonce 생성 실패 메시지를 출력합니다. */ + fprintf(stderr, "Could not generate a secure random nonce.\n"); + + /* cleanup으로 이동합니다. */ + goto cleanup; + } + + /* + * 출력 파일을 바이너리 쓰기 모드로 엽니다. + * "wb"는 기존 파일이 있으면 덮어씁니다. + */ + output = fopen(output_path, "wb"); + + /* 출력 파일 생성에 실패했는지 확인합니다. */ + if (output == NULL) { + /* 오류 메시지를 출력합니다. */ + fprintf(stderr, "Could not create the output file.\n"); + + /* cleanup으로 이동합니다. */ + goto cleanup; + } + + /* + * 원본 파일 크기를 8바이트 little-endian 형식으로 변환합니다. + * 이 값은 복호화할 때 원래 크기 확인 등에 사용할 수 있습니다. + */ encode_u64_le(size_bytes, (uint64_t)input_size); + + /* + * 암호문 파일 헤더를 씁니다. + * + * 헤더 구성: + * 1. magic : Gerbera 파일인지 식별 + * 2. nonce : 복호화 시 같은 키스트림을 재생성하기 위한 값 + * 3. size_bytes : 원본 파일 크기 + */ if (fwrite(magic, 1, sizeof(magic), output) != sizeof(magic) || fwrite(nonce, 1, sizeof(nonce), output) != sizeof(nonce) || fwrite(size_bytes, 1, sizeof(size_bytes), output) != sizeof(size_bytes)) { + /* 헤더 쓰기에 실패한 경우 오류 메시지를 출력합니다. */ fprintf(stderr, "Could not write the encrypted file header.\n"); + + /* cleanup으로 이동합니다. */ goto cleanup; } + /* + * 스트림 암호 상태를 초기화합니다. + * password와 nonce가 같으면 복호화 때 같은 키스트림이 생성됩니다. + */ gerbera_cipher_init(&cipher, password, nonce); + + /* + * 인증 태그 계산 상태를 초기화합니다. + * 암호문이 변조되었는지 확인하기 위해 사용됩니다. + */ gerbera_auth_init(&auth, password, nonce); + + /* + * magic 값을 인증 대상에 넣습니다. + * 이렇게 하면 파일 서명이 바뀌어도 인증 태그 검증에 실패합니다. + */ gerbera_auth_update(&auth, magic, sizeof(magic)); + + /* + * nonce도 인증 대상에 넣습니다. + * nonce가 바뀌면 복호화 키스트림이 달라지므로 반드시 보호해야 합니다. + */ gerbera_auth_update(&auth, nonce, sizeof(nonce)); + + /* + * 원본 파일 크기 필드도 인증 대상에 넣습니다. + * size_bytes가 변조되어도 태그 검증에서 잡을 수 있습니다. + */ gerbera_auth_update(&auth, size_bytes, sizeof(size_bytes)); + /* + * 입력 파일을 buffer 크기만큼 반복해서 읽습니다. + * 큰 파일도 한 번에 메모리에 올리지 않고 처리할 수 있습니다. + */ while ((count = fread(buffer, 1, sizeof(buffer), input)) > 0) { + /* + * 읽어온 count 바이트를 제자리에서 암호화합니다. + * buffer 안의 평문이 암호문으로 바뀝니다. + */ gerbera_cipher_apply(&cipher, buffer, count); + + /* + * 암호화된 바이트를 인증 상태에 넣습니다. + * 즉, 이 구현은 평문이 아니라 암호문을 인증합니다. + */ gerbera_auth_update(&auth, buffer, count); + + /* + * 암호화된 청크를 출력 파일에 씁니다. + */ if (fwrite(buffer, 1, count, output) != count) { + /* 쓰기 실패 메시지를 출력합니다. */ fprintf(stderr, "Could not write the encrypted data.\n"); + + /* cleanup으로 이동합니다. */ goto cleanup; } } + + /* + * fread() 반복문이 끝난 이유가 정상 EOF인지, + * 아니면 읽기 오류 때문인지 확인합니다. + */ if (ferror(input)) { + /* 읽기 오류 메시지를 출력합니다. */ fprintf(stderr, "An error occurred while reading the input file.\n"); + + /* cleanup으로 이동합니다. */ goto cleanup; } + + /* + * 지금까지 인증 상태에 넣은 값들로 최종 태그를 만듭니다. + * + * 인증 대상: + * magic + * nonce + * size_bytes + * 암호문 전체 + */ gerbera_auth_finish(&auth, tag); + + /* + * 최종 인증 태그를 파일 맨 끝에 붙입니다. + * 복호화할 때 이 태그를 다시 계산해 비교하면 변조 여부를 확인할 수 있습니다. + */ if (fwrite(tag, 1, sizeof(tag), output) != sizeof(tag)) { + /* 태그 쓰기 실패 메시지를 출력합니다. */ fprintf(stderr, "Could not write the authentication tag.\n"); + + /* cleanup으로 이동합니다. */ goto cleanup; } + + /* + * 여기까지 왔다면 암호화, 인증 태그 생성, 파일 쓰기가 모두 성공한 것입니다. + */ result = 0; cleanup: + /* + * input이 열려 있다면 닫습니다. + * NULL이면 아직 열리지 않았다는 뜻이므로 닫지 않습니다. + */ if (input != NULL) fclose(input); + + /* + * output이 열려 있다면 닫습니다. + * fclose()가 실패하면 출력 파일이 완전히 저장되지 않았을 수 있으므로 실패 처리합니다. + */ if (output != NULL && fclose(output) != 0) result = 1; + + /* + * 작업이 실패했다면 output_path를 삭제합니다. + * 중간까지만 쓰인 파일을 정상 암호문으로 착각하지 않게 하기 위함입니다. + */ if (result != 0) remove(output_path); + + /* 최종 결과를 반환합니다. */ return result; -} +} \ No newline at end of file diff --git a/src/main.c b/src/main.c index 20b5c3d..d218c31 100644 --- a/src/main.c +++ b/src/main.c @@ -1,10 +1,20 @@ -#include "gerbera.h" +#include "gerbera.h" /* encrypt_file(), decrypt_file() 선언을 가져옵니다. */ -#include -#include +#include /* printf(), fprintf(), fgets(), stderr, stdin 사용 */ +#include /* strcmp(), strcspn() 사용 */ +/* + * 프로그램 사용법을 출력합니다. + * + * program: + * argv[0]으로 받은 실행 파일 이름입니다. + */ static void print_usage(const char *program) { + /* + * 사용법은 일반 출력(stdout)이 아니라 에러 출력(stderr)으로 보냅니다. + * 잘못된 인자 사용은 정상 결과 출력이 아니기 때문입니다. + */ fprintf(stderr, "Usage:\n" " %s encrypt \n" @@ -12,80 +22,234 @@ static void print_usage(const char *program) program, program); } +/* + * 명령줄 인자 없이 실행했을 때 사용하는 대화형 모드입니다. + * + * 사용자에게 mode, input, output, password를 직접 입력받습니다. + * + * 반환값: + * 0 = 성공 + * 1 = 입력 실패 또는 암호화/복호화 실패 + * 2 = 사용법 오류 + */ static int interactive_mode(void) { + /* + * 작업 모드를 저장할 버퍼입니다. + * "encrypt" 또는 "decrypt"가 들어갈 수 있습니다. + */ char mode[16]; + + /* + * 입력 파일 경로를 저장할 버퍼입니다. + * fgets()로 읽기 때문에 공백이 포함된 경로도 받을 수 있습니다. + */ char input[1024]; + + /* + * 출력 파일 경로를 저장할 버퍼입니다. + */ char output[1024]; + + /* + * 비밀번호를 저장할 버퍼입니다. + * + * 주의: + * 이 방식은 입력한 비밀번호가 화면에 그대로 보입니다. + * 실제 프로그램에서는 터미널 echo를 끄는 방식을 사용할 수 있습니다. + */ char password[1024]; + /* 사용자에게 작업 모드를 입력하라고 안내합니다. */ printf("Mode (encrypt/decrypt): "); - if (!fgets(mode, sizeof(mode), stdin)) - return 1; + /* + * mode 한 줄을 읽습니다. + * 실패하면 더 진행할 수 없으므로 1을 반환합니다. + */ + if (!fgets(mode, sizeof(mode), stdin)) return 1; + + /* 사용자에게 입력 파일 경로를 입력하라고 안내합니다. */ printf("Input file: "); - if (!fgets(input, sizeof(input), stdin)) - return 1; + /* + * 입력 파일 경로 한 줄을 읽습니다. + * 실패하면 1을 반환합니다. + */ + if (!fgets(input, sizeof(input), stdin)) return 1; + + /* 사용자에게 출력 파일 경로를 입력하라고 안내합니다. */ printf("Output file: "); - if (!fgets(output, sizeof(output), stdin)) - return 1; + /* + * 출력 파일 경로 한 줄을 읽습니다. + * 실패하면 1을 반환합니다. + */ + if (!fgets(output, sizeof(output), stdin)) return 1; + + /* 사용자에게 비밀번호를 입력하라고 안내합니다. */ printf("Password: "); - if (!fgets(password, sizeof(password), stdin)) - return 1; + /* + * 비밀번호 한 줄을 읽습니다. + * 실패하면 1을 반환합니다. + */ + if (!fgets(password, sizeof(password), stdin)) return 1; + + /* + * fgets()는 입력 끝의 개행 문자도 버퍼에 저장합니다. + * strcspn(mode, "\r\n")은 mode에서 처음으로 \r 또는 \n이 나오는 위치를 찾습니다. + * 그 위치에 '\0'을 넣어서 줄바꿈을 제거합니다. + */ mode[strcspn(mode, "\r\n")] = '\0'; + + /* input 문자열 끝의 줄바꿈을 제거합니다. */ input[strcspn(input, "\r\n")] = '\0'; + + /* output 문자열 끝의 줄바꿈을 제거합니다. */ output[strcspn(output, "\r\n")] = '\0'; + + /* password 문자열 끝의 줄바꿈을 제거합니다. */ password[strcspn(password, "\r\n")] = '\0'; + /* + * 비밀번호 첫 글자가 널 문자면 빈 문자열입니다. + * 빈 비밀번호는 너무 쉽게 실수할 수 있으므로 거부합니다. + */ if (password[0] == '\0') { + /* 빈 비밀번호 오류 메시지를 출력합니다. */ fprintf(stderr, "The password cannot be empty.\n"); + + /* 사용법 오류에 가까우므로 2를 반환합니다. */ return 2; } + /* + * 입력 파일 경로와 출력 파일 경로가 완전히 같은지 확인합니다. + * 같은 파일을 읽으면서 동시에 쓰면 원본이 망가질 수 있습니다. + */ if (strcmp(input, output) == 0) { + /* 입력과 출력 경로가 같다는 오류 메시지를 출력합니다. */ fprintf(stderr, "The paths of the input and output files cannot be the same.\n"); + + /* 잘못된 사용이므로 2를 반환합니다. */ return 2; } + /* + * mode가 "encrypt"이면 암호화를 실행합니다. + * encrypt_file()의 반환값을 그대로 반환합니다. + */ if (strcmp(mode, "encrypt") == 0) return encrypt_file(input, output, password); + /* + * mode가 "decrypt"이면 복호화를 실행합니다. + * decrypt_file()의 반환값을 그대로 반환합니다. + */ if (strcmp(mode, "decrypt") == 0) return decrypt_file(input, output, password); + /* + * encrypt도 decrypt도 아니면 알 수 없는 모드입니다. + */ fprintf(stderr, "Unknown mode. Use 'encrypt' or 'decrypt'.\n"); + + /* 잘못된 사용이므로 2를 반환합니다. */ return 2; } +/* + * 프로그램의 시작점입니다. + * + * argc: + * 명령줄 인자의 개수입니다. + * + * argv: + * 명령줄 인자 문자열 배열입니다. + * + * 사용 예: + * ./gerbera encrypt input.txt output.gerbera password + * ./gerbera decrypt output.gerbera input.txt password + */ int main(int argc, char *argv[]) { + /* + * argc가 1이면 프로그램 이름만 있고 추가 인자가 없는 상태입니다. + * 이 경우 대화형 모드로 전환합니다. + */ if (argc == 1) return interactive_mode(); + /* + * 명령줄 모드에서는 총 5개의 인자가 필요합니다. + * + * argv[0] = 프로그램 이름 + * argv[1] = encrypt 또는 decrypt + * argv[2] = 입력 파일 경로 + * argv[3] = 출력 파일 경로 + * argv[4] = 비밀번호 + */ if (argc != 5) { + /* 사용법을 출력합니다. */ print_usage(argv[0]); + + /* 인자 개수가 잘못되었으므로 2를 반환합니다. */ return 2; } + /* + * argv[4]는 비밀번호입니다. + * 첫 글자가 널 문자면 빈 비밀번호입니다. + */ if (argv[4][0] == '\0') { + /* 빈 비밀번호 오류 메시지를 출력합니다. */ fprintf(stderr, "The password cannot be empty.\n"); + + /* 잘못된 사용이므로 2를 반환합니다. */ return 2; } + /* + * argv[2]는 입력 파일 경로, + * argv[3]은 출력 파일 경로입니다. + * + * 두 경로 문자열이 같으면 원본 파일을 덮어쓸 위험이 있습니다. + */ if (strcmp(argv[2], argv[3]) == 0) { + /* 입력과 출력 경로가 같다는 오류 메시지를 출력합니다. */ fprintf(stderr, "The paths of the input and output files cannot be the same.\n"); + + /* 잘못된 사용이므로 2를 반환합니다. */ return 2; } + /* + * 첫 번째 명령 인자가 "encrypt"이면 암호화를 실행합니다. + * + * argv[2] = 입력 파일 + * argv[3] = 출력 파일 + * argv[4] = 비밀번호 + */ if (strcmp(argv[1], "encrypt") == 0) return encrypt_file(argv[2], argv[3], argv[4]); + /* + * 첫 번째 명령 인자가 "decrypt"이면 복호화를 실행합니다. + * + * argv[2] = 입력 파일 + * argv[3] = 출력 파일 + * argv[4] = 비밀번호 + */ if (strcmp(argv[1], "decrypt") == 0) return decrypt_file(argv[2], argv[3], argv[4]); + /* + * encrypt도 decrypt도 아니면 잘못된 명령입니다. + * 사용법을 다시 출력합니다. + */ print_usage(argv[0]); + + /* 잘못된 사용이므로 2를 반환합니다. */ return 2; -} +} \ No newline at end of file