imnyang/Gerbera
Watch
1
0
Fork
You've already forked Gerbera
0
This commit is contained in:
암냥 2026-06-25 13:48:22 +09:00
commit 303192b198
9 changed files with 1154 additions and 34 deletions

113
.gitignore vendored
View file

@ -2,4 +2,117 @@ tests/*
!test/test.sh !test/test.sh
build/ build/
.vscode .vscode
# 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 .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

View file

@ -1,29 +1,37 @@
# 컴파일러와 플래그 기본값은 환경 변수나 make 인자로 덮어쓸 수 있습니다.
CC ?= cc CC ?= cc
CPPFLAGS ?= -Iinclude CPPFLAGS ?= -Iinclude
CFLAGS ?= -std=c11 -O2 -Wall -Wextra -Wpedantic CFLAGS ?= -std=c11 -O2 -Wall -Wextra -Wpedantic
LDLIBS ?= LDLIBS ?=
# Windows 빌드는 호환 컴파일러/링커를 사용할 때 bcrypt가 필요합니다.
ifeq ($(OS),Windows_NT) ifeq ($(OS),Windows_NT)
LDLIBS += -lbcrypt LDLIBS += -lbcrypt
endif endif
# 최종 실행 파일 경로, 소스 목록, 여기서 파생되는 오브젝트 파일 경로입니다.
TARGET := build/gerbera TARGET := build/gerbera
SOURCES := src/main.c src/encrypt.c src/decrypt.c src/cipher.c SOURCES := src/main.c src/encrypt.c src/decrypt.c src/cipher.c
OBJECTS := $(SOURCES:src/%.c=build/%.o) OBJECTS := $(SOURCES:src/%.c=build/%.o)
# 모든 오브젝트 파일을 컴파일한 뒤 실행 파일을 링크합니다.
$(TARGET): $(OBJECTS) $(TARGET): $(OBJECTS)
$(CC) $(CFLAGS) -o $@ $^ $(LDLIBS) $(CC) $(CFLAGS) -o $@ $^ $(LDLIBS)
# 각 C 파일을 build/<name>.o로 컴파일하며, 필요하면 build/를 먼저 만듭니다.
build/%.o: src/%.c include/gerbera.h | build build/%.o: src/%.c include/gerbera.h | build
$(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $< $(CC) $(CPPFLAGS) $(CFLAGS) -c -o $@ $<
# 컴파일이나 링크 전에 출력 디렉터리가 존재하도록 보장합니다.
build: build:
mkdir -p build mkdir -p build
.PHONY: clean test .PHONY: clean test
# 방금 빌드한 실행 파일로 셸 테스트 모음을 실행합니다.
test: $(TARGET) test: $(TARGET)
GERBERA_BIN=./$(TARGET) sh tests/test.sh GERBERA_BIN=./$(TARGET) sh tests/test.sh
# 생성된 모든 빌드 산출물을 삭제합니다.
clean: clean:
rm -rf build rm -rf build

View file

@ -1,16 +1,23 @@
#ifndef GERBERA_H #ifndef GERBERA_H
#define GERBERA_H #define GERBERA_H
/* 명령줄 진입점과 각 모듈이 함께 쓰는 공개 선언입니다. */
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
/* Gerbera 파일 형식과 메모리 작업 버퍼에서 사용하는 고정 크기입니다. */
#define GERBERA_MAGIC_SIZE 10 #define GERBERA_MAGIC_SIZE 10
#define GERBERA_NONCE_SIZE 16 #define GERBERA_NONCE_SIZE 16
#define GERBERA_TAG_SIZE 16 #define GERBERA_TAG_SIZE 16
#define GERBERA_BUFFER_SIZE 4096 #define GERBERA_BUFFER_SIZE 4096
/* 헤더에는 매직 바이트, nonce, 원문 길이가 저장됩니다. */
#define GERBERA_HEADER_SIZE (GERBERA_MAGIC_SIZE + GERBERA_NONCE_SIZE + 8) #define GERBERA_HEADER_SIZE (GERBERA_MAGIC_SIZE + GERBERA_NONCE_SIZE + 8)
/* 암호화 파일은 데이터 앞에 헤더를, 뒤에 인증 태그를 붙입니다. */
#define GERBERA_OVERHEAD_SIZE (GERBERA_HEADER_SIZE + GERBERA_TAG_SIZE) #define GERBERA_OVERHEAD_SIZE (GERBERA_HEADER_SIZE + GERBERA_TAG_SIZE)
/* 스트림 암호 상태: 순열 테이블, 파생 키, 인덱스, 바이트 위치입니다. */
typedef struct { typedef struct {
uint8_t state[256]; uint8_t state[256];
uint8_t key[32]; uint8_t key[32];
@ -19,22 +26,34 @@ typedef struct {
uint64_t position; uint64_t position;
} GerberaCipher; } GerberaCipher;
/* 고정 크기 인증 태그를 계산하고 검증하는 상태입니다. */
typedef struct { typedef struct {
uint8_t state[32]; uint8_t state[32];
uint64_t length; uint64_t length;
} GerberaAuth; } GerberaAuth;
/* 비밀번호와 파일별 nonce로 암호 스트림을 초기화합니다. */
void gerbera_cipher_init(GerberaCipher *ctx, const char *password, void gerbera_cipher_init(GerberaCipher *ctx, const char *password,
const uint8_t nonce[GERBERA_NONCE_SIZE]); const uint8_t nonce[GERBERA_NONCE_SIZE]);
/* 생성된 키스트림을 data에 XOR합니다. 다시 호출하면 스트림이 이어집니다. */
void gerbera_cipher_apply(GerberaCipher *ctx, uint8_t data[], size_t length); void gerbera_cipher_apply(GerberaCipher *ctx, uint8_t data[], size_t length);
/* 같은 비밀번호와 nonce로 인증 누산기를 초기화합니다. */
void gerbera_auth_init(GerberaAuth *ctx, const char *password, void gerbera_auth_init(GerberaAuth *ctx, const char *password,
const uint8_t nonce[GERBERA_NONCE_SIZE]); const uint8_t nonce[GERBERA_NONCE_SIZE]);
/* 파일에 기록되는 순서대로 바이트를 인증 누산기에 넣습니다. */
void gerbera_auth_update(GerberaAuth *ctx, const uint8_t data[], size_t length); 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]); 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[], int gerbera_constant_time_equal(const uint8_t a[], const uint8_t b[],
size_t length); size_t length);
/* 파일 전체를 암호화하거나 복호화하며, 성공 시 0, 오류 시 0이 아닌 값을 반환합니다. */
int encrypt_file(const char *input_path, const char *output_path, int encrypt_file(const char *input_path, const char *output_path,
const char *password); const char *password);
int decrypt_file(const char *input_path, const char *output_path, int decrypt_file(const char *input_path, const char *output_path,

View file

@ -1,21 +1,28 @@
@echo off @echo off
rem 환경 변경이 이 스크립트 실행 안에만 머물도록 합니다.
setlocal setlocal
rem Makefile과 POSIX 스크립트처럼 실행 파일을 build\에 둡니다.
if not exist build mkdir build if not exist build mkdir build
rem Visual Studio 컴파일러가 있으면 우선 사용합니다.
where cl >nul 2>nul where cl >nul 2>nul
if %errorlevel% equ 0 goto build_msvc if %errorlevel% equ 0 goto build_msvc
rem GCC가 설치되어 있고 PATH에서 보이면 다음 후보로 사용합니다.
where gcc >nul 2>nul where gcc >nul 2>nul
if %errorlevel% equ 0 goto build_gcc if %errorlevel% equ 0 goto build_gcc
rem 세 번째 지원 Windows 컴파일러로 Clang을 사용합니다.
where clang >nul 2>nul where clang >nul 2>nul
if %errorlevel% equ 0 goto build_clang if %errorlevel% equ 0 goto build_clang
rem 지원되는 컴파일러를 찾지 못했으므로 명확한 메시지를 출력하고 중단합니다.
echo C compiler not found. Install Visual Studio Build Tools, GCC, or Clang. echo C compiler not found. Install Visual Studio Build Tools, GCC, or Clang.
exit /b 1 exit /b 1
:build_msvc :build_msvc
rem MSVC는 build\ 안에 gerbera.exe를 쓰고 bcrypt.lib를 명시적으로 링크합니다.
pushd build pushd build
cl /nologo /std:c11 /O2 /W4 /I..\include ^ cl /nologo /std:c11 /O2 /W4 /I..\include ^
..\src\main.c ..\src\encrypt.c ..\src\decrypt.c ..\src\cipher.c ^ ..\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 goto success
:build_gcc :build_gcc
rem GCC는 POSIX 빌드와 같은 경고 옵션을 쓰고 bcrypt를 링크합니다.
gcc -Iinclude -std=c11 -O2 -Wall -Wextra -Wpedantic ^ gcc -Iinclude -std=c11 -O2 -Wall -Wextra -Wpedantic ^
src\main.c src\encrypt.c src\decrypt.c src\cipher.c ^ src\main.c src\encrypt.c src\decrypt.c src\cipher.c ^
-o build\gerbera.exe -lbcrypt -o build\gerbera.exe -lbcrypt
@ -33,11 +41,13 @@ if not %errorlevel% equ 0 exit /b %errorlevel%
goto success goto success
:build_clang :build_clang
rem Clang은 이 작은 프로젝트에서 GCC 방식 명령줄을 그대로 따릅니다.
clang -Iinclude -std=c11 -O2 -Wall -Wextra -Wpedantic ^ clang -Iinclude -std=c11 -O2 -Wall -Wextra -Wpedantic ^
src\main.c src\encrypt.c src\decrypt.c src\cipher.c ^ src\main.c src\encrypt.c src\decrypt.c src\cipher.c ^
-o build\gerbera.exe -lbcrypt -o build\gerbera.exe -lbcrypt
if not %errorlevel% equ 0 exit /b %errorlevel% if not %errorlevel% equ 0 exit /b %errorlevel%
:success :success
rem 모든 컴파일러 선택지가 공유하는 성공 경로입니다.
echo Build complete: build\gerbera.exe echo Build complete: build\gerbera.exe
exit /b 0 exit /b 0

View file

@ -1,9 +1,15 @@
#!/bin/sh #!/bin/sh
# 실패한 명령이나 정의되지 않은 변수에서 즉시 종료합니다.
set -eu set -eu
# 호출자가 CC=...로 컴파일러를 고를 수 있고, 기본값은 cc입니다.
compiler="${CC:-cc}" compiler="${CC:-cc}"
# 컴파일 산출물은 소스 트리 밖의 build/에 둡니다.
mkdir -p build mkdir -p build
# 모든 C 번역 단위를 묶어 Gerbera 명령 하나를 빌드합니다.
"$compiler" \ "$compiler" \
-Iinclude \ -Iinclude \
-std=c11 \ -std=c11 \
@ -17,4 +23,5 @@ mkdir -p build
src/cipher.c \ src/cipher.c \
-o build/gerbera -o build/gerbera
# 실행 파일이 어디에 만들어졌는지 알려 줍니다.
echo "Build complete: build/gerbera" echo "Build complete: build/gerbera"

View file

@ -1,145 +1,446 @@
#include "gerbera.h" #include "gerbera.h" /* GerberaCipher, GerberaAuth, 상수 크기 등을 정의한 헤더 */
#include <string.h> #include <string.h> /* strlen(), memcpy() 사용 */
/* uint8_t는 8비트 정수이므로, 이 함수는 1바이트 값을 왼쪽으로 회전합니다. */
static uint8_t rotate_left8(uint8_t value, unsigned count) static uint8_t rotate_left8(uint8_t value, unsigned count)
{ {
/* count가 8 이상이면 8비트 회전에 맞게 0~7 범위로 줄입니다. */
count &= 7U; count &= 7U;
/*
* 왼쪽으로 count만큼 민 비트와,
* 오른쪽으로 8-count만큼 민 비트를 합쳐서 회전 효과를 만듭니다.
*
* ((8U - count) & 7U)를 쓰는 이유:
* count가 0일 때 value >> 8이 되면 C에서 미정의 동작이므로,
* 8을 다시 0으로 바꿔 안전하게 만듭니다.
*/
return (uint8_t)((value << count) | (value >> ((8U - count) & 7U))); return (uint8_t)((value << count) | (value >> ((8U - count) & 7U)));
} }
/* /*
* Password bytes are folded into a 32-byte array many times. This is an * 비밀번호와 nonce를 이용해 32바이트 키를 만듭니다.
* educational, original construction; it is not a replacement for a *
* reviewed password KDF such as Argon2. * key : 결과로 채워질 32바이트 키
* password : 사용자가 입력한 비밀번호
* nonce : 파일마다 달라지는 랜덤값
* domain : 암호화용 키와 인증용 키를 서로 다르게 만들기 위한 구분값
*/ */
static void derive_key(uint8_t key[32], const char *password, static void derive_key(uint8_t key[32], const char *password,
const uint8_t nonce[GERBERA_NONCE_SIZE], uint8_t domain) const uint8_t nonce[GERBERA_NONCE_SIZE], uint8_t domain)
{ {
/*
* 키 파생의 시작점으로 사용할 고정된 32바이트 값입니다.
* 전부 0에서 시작하면 섞이는 정도가 약해질 수 있으므로,
* 0이 아닌 값들로 시작합니다.
*/
static const uint8_t initial[32] = { 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, 0x7a, 0x75, 0x6b, 0x69, 0x4e, 0x41, 0x45, 0x31,
0x9d, 0x37, 0x6b, 0xa1, 0xf4, 0x52, 0xc8, 0x0e, 0x9d, 0x37, 0x6b, 0xa1, 0xf4, 0x52, 0xc8, 0x0e,
0x73, 0xbf, 0x25, 0xda, 0x86, 0x19, 0xe0, 0x4c 0x73, 0xbf, 0x25, 0xda, 0x86, 0x19, 0xe0, 0x4c
}; };
/* 비밀번호 길이를 미리 구해 반복문에서 사용합니다. */
size_t password_length = strlen(password); size_t password_length = strlen(password);
/* 키 파생을 여러 번 반복하기 위한 라운드 변수입니다. */
unsigned round; unsigned round;
/* 비밀번호의 각 바이트를 순회하기 위한 인덱스입니다. */
size_t p; size_t p;
/* key 배열을 initial 값으로 초기화합니다. */
memcpy(key, initial, sizeof(initial)); memcpy(key, initial, sizeof(initial));
/*
* domain 값을 key[0]에 섞습니다.
* 이렇게 하면 같은 password와 nonce를 써도,
* 암호화용 키와 인증용 키가 서로 달라집니다.
*/
key[0] ^= domain; key[0] ^= domain;
/*
* 비밀번호와 nonce를 8192번 반복해서 섞습니다.
* 반복 횟수가 많을수록 단순한 비밀번호를 바로 키로 쓰는 것보다 느려져서,
* 무차별 대입 공격 비용을 조금 높일 수 있습니다.
*/
for (round = 0; round < 8192U; ++round) { for (round = 0; round < 8192U; ++round) {
/* 비밀번호의 모든 바이트를 하나씩 키 상태에 섞습니다. */
for (p = 0; p < password_length; ++p) { for (p = 0; p < password_length; ++p) {
/*
* a는 현재 갱신할 key 위치입니다.
* & 31U는 32로 나눈 나머지와 같아서 0~31 범위가 됩니다.
*/
size_t a = (p + round) & 31U; size_t a = (p + round) & 31U;
/* b는 a에서 11칸 떨어진 위치입니다. */
size_t b = (a + 11U) & 31U; size_t b = (a + 11U) & 31U;
/* c는 a에서 23칸 떨어진 위치입니다. */
size_t c = (a + 23U) & 31U; size_t c = (a + 23U) & 31U;
/*
* mixed는 현재 키 바이트, 비밀번호 바이트, nonce 바이트,
* 라운드 번호를 모두 더해서 만든 중간값입니다.
*
* uint8_t로 캐스팅하므로 0~255 범위만 남습니다.
*/
uint8_t mixed = (uint8_t)(key[b] + (uint8_t)password[p] + uint8_t mixed = (uint8_t)(key[b] + (uint8_t)password[p] +
nonce[(p + round) & 15U] + nonce[(p + round) & 15U] +
(uint8_t)round); (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), key[a] = (uint8_t)(rotate_left8((uint8_t)(key[a] ^ mixed),
(unsigned)(key[c] & 7U)) + key[c]); (unsigned)(key[c] & 7U)) + key[c]);
} }
/*
* 비밀번호가 매우 짧거나 비어 있더라도,
* 매 라운드마다 key 내부 값끼리 한 번 더 섞이도록 합니다.
*/
key[round & 31U] ^= rotate_left8(key[(round + 17U) & 31U], round & 7U); key[round & 31U] ^= rotate_left8(key[(round + 17U) & 31U], round & 7U);
} }
} }
/*
* 스트림 암호 상태를 초기화합니다.
*
* ctx : 암호화 상태 구조체
* password : 비밀번호
* nonce : 파일마다 다른 nonce
*/
void gerbera_cipher_init(GerberaCipher *ctx, const char *password, 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; unsigned n;
/* RC4 계열 알고리즘에서 사용하는 두 번째 인덱스 역할입니다. */
uint8_t j = 0; uint8_t j = 0;
/*
* 암호화용 키를 파생합니다.
* domain 값 0x43은 cipher의 C라고 생각하면 됩니다.
*/
derive_key(ctx->key, password, nonce, 0x43U); derive_key(ctx->key, password, nonce, 0x43U);
/*
* state 배열을 0, 1, 2, ..., 255로 채웁니다.
* 이 배열은 이후 키에 따라 섞이는 순열 테이블입니다.
*/
for (n = 0; n < 256U; ++n) { for (n = 0; n < 256U; ++n) {
ctx->state[n] = (uint8_t)n; ctx->state[n] = (uint8_t)n;
} }
/*
* 파생된 key와 nonce를 이용해 state 배열을 섞습니다.
* 256번이 아니라 1024번 반복해서 같은 테이블을 여러 차례 뒤섞습니다.
*/
for (n = 0; n < 1024U; ++n) { for (n = 0; n < 1024U; ++n) {
/*
* i는 현재 섞을 state 위치입니다.
* n이 256 이상이어도 uint8_t로 줄어들어 0~255를 반복합니다.
*/
uint8_t i = (uint8_t)n; uint8_t i = (uint8_t)n;
/* state[i]와 state[j]를 교환할 때 임시로 저장하는 변수입니다. */
uint8_t temporary; 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] + j = (uint8_t)(j + ctx->state[i] + ctx->key[n & 31U] +
nonce[n & 15U]); nonce[n & 15U]);
/* state[i] 값을 임시 변수에 저장합니다. */
temporary = ctx->state[i]; temporary = ctx->state[i];
/* state[j] 값을 state[i] 위치로 옮깁니다. */
ctx->state[i] = ctx->state[j]; ctx->state[i] = ctx->state[j];
/* 임시로 저장했던 원래 state[i] 값을 state[j]에 넣습니다. */
ctx->state[j] = temporary; ctx->state[j] = temporary;
} }
/* 키스트림 생성에 사용할 i 인덱스를 0으로 초기화합니다. */
ctx->i = 0; ctx->i = 0;
/*
* j는 섞기 과정이 끝난 뒤의 값을 그대로 사용합니다.
* 이렇게 하면 초기 순열 상태와 j 상태가 같이 맞춰집니다.
*/
ctx->j = j; ctx->j = j;
/* 지금까지 처리한 바이트 위치를 0으로 초기화합니다. */
ctx->position = 0; ctx->position = 0;
} }
/*
* data 배열에 스트림 암호를 적용합니다.
*
* XOR 기반이므로 같은 함수로 암호화와 복호화를 모두 수행할 수 있습니다.
*/
void gerbera_cipher_apply(GerberaCipher *ctx, uint8_t data[], size_t length) void gerbera_cipher_apply(GerberaCipher *ctx, uint8_t data[], size_t length)
{ {
/* data의 각 바이트를 순회하기 위한 인덱스입니다. */
size_t n; size_t n;
/* 입력 데이터 길이만큼 한 바이트씩 처리합니다. */
for (n = 0; n < length; ++n) { for (n = 0; n < length; ++n) {
/* state 값을 교환할 때 사용할 임시 변수입니다. */
uint8_t temporary; uint8_t temporary;
/* 이번 위치에서 생성된 키스트림 바이트입니다. */
uint8_t stream; uint8_t stream;
/* i를 한 칸 전진시킵니다. */
ctx->i = (uint8_t)(ctx->i + 1U); ctx->i = (uint8_t)(ctx->i + 1U);
/*
* j도 전진시킵니다.
*
* 현재 state[i] 값과 key의 현재 위치 값을 함께 섞어
* 키스트림이 위치에 따라 달라지도록 합니다.
*/
ctx->j = (uint8_t)(ctx->j + ctx->state[ctx->i] + ctx->j = (uint8_t)(ctx->j + ctx->state[ctx->i] +
ctx->key[ctx->position & 31U]); ctx->key[ctx->position & 31U]);
/* state[i]를 임시 변수에 저장합니다. */
temporary = ctx->state[ctx->i]; temporary = ctx->state[ctx->i];
/* state[j] 값을 state[i] 위치로 옮깁니다. */
ctx->state[ctx->i] = ctx->state[ctx->j]; ctx->state[ctx->i] = ctx->state[ctx->j];
/* 원래 state[i] 값을 state[j]에 넣어 두 값을 교환합니다. */
ctx->state[ctx->j] = temporary; ctx->state[ctx->j] = temporary;
/*
* 교환 이후의 state[i]와 state[j] 값을 더해서
* state 배열에서 키스트림 후보 바이트 하나를 가져옵니다.
*/
stream = ctx->state[(uint8_t)(ctx->state[ctx->i] + stream = ctx->state[(uint8_t)(ctx->state[ctx->i] +
ctx->state[ctx->j])]; 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], stream ^= rotate_left8(ctx->key[(ctx->position + stream) & 31U],
(unsigned)(ctx->position & 7U)); (unsigned)(ctx->position & 7U));
/*
* 원본 데이터 바이트와 키스트림 바이트를 XOR합니다.
*
* 암호화:
* 평문 ^ 키스트림 = 암호문
*
* 복호화:
* 암호문 ^ 같은 키스트림 = 평문
*/
data[n] ^= stream; data[n] ^= stream;
/* 처리한 바이트 수를 1 증가시킵니다. */
++ctx->position; ++ctx->position;
} }
} }
/*
* 인증 태그를 만들기 위한 상태를 초기화합니다.
*
* 암호화와 별개로 데이터 무결성 확인용 태그를 만들 때 사용합니다.
*/
void gerbera_auth_init(GerberaAuth *ctx, const char *password, 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); derive_key(ctx->state, password, nonce, 0x41U);
/* 아직 처리한 데이터가 없으므로 길이는 0입니다. */
ctx->length = 0; ctx->length = 0;
} }
/*
* 인증 상태에 데이터를 흡수합니다.
*
* data의 모든 바이트가 ctx->state에 누적되어,
* 최종적으로 tag 생성에 영향을 줍니다.
*/
void gerbera_auth_update(GerberaAuth *ctx, const uint8_t data[], size_t length) void gerbera_auth_update(GerberaAuth *ctx, const uint8_t data[], size_t length)
{ {
/* data 배열을 순회할 인덱스입니다. */
size_t n; size_t n;
/* 입력 데이터를 한 바이트씩 처리합니다. */
for (n = 0; n < length; ++n) { for (n = 0; n < length; ++n) {
/*
* a는 이번 바이트가 주로 반영될 state 위치입니다.
* 전체 처리 길이를 기준으로 0~31 위치를 순환합니다.
*/
size_t a = (size_t)(ctx->length & 31U); size_t a = (size_t)(ctx->length & 31U);
/* b는 a에서 7칸 떨어진 위치입니다. */
size_t b = (a + 7U) & 31U; size_t b = (a + 7U) & 31U;
/* c는 a에서 19칸 떨어진 위치입니다. */
size_t c = (a + 19U) & 31U; 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( ctx->state[a] = (uint8_t)(rotate_left8(
(uint8_t)(ctx->state[a] ^ data[n] ^ ctx->state[c]), (uint8_t)(ctx->state[a] ^ data[n] ^ ctx->state[c]),
(unsigned)(ctx->state[b] & 7U)) + ctx->state[b] + (uint8_t)a); (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]), ctx->state[c] ^= rotate_left8((uint8_t)(data[n] + ctx->state[a]),
(unsigned)(a & 7U)); (unsigned)(a & 7U));
/* 전체 처리 길이를 1 증가시킵니다. */
++ctx->length; ++ctx->length;
} }
} }
/*
* 지금까지 auth_update로 흡수한 데이터에서 최종 인증 태그를 만듭니다.
*/
void gerbera_auth_finish(GerberaAuth *ctx, uint8_t tag[GERBERA_TAG_SIZE]) void gerbera_auth_finish(GerberaAuth *ctx, uint8_t tag[GERBERA_TAG_SIZE])
{ {
/*
* 메시지 길이를 8바이트 little-endian 형태로 저장할 배열입니다.
* 길이를 태그에 포함하면, 뒤에 0바이트가 붙은 메시지와 구분할 수 있습니다.
*/
uint8_t trailer[8]; uint8_t trailer[8];
/* 반복문에서 사용할 인덱스입니다. */
unsigned n; unsigned n;
/*
* ctx->length를 8바이트로 나누어 trailer에 저장합니다.
*
* n = 0일 때 하위 8비트,
* n = 1일 때 그 다음 8비트,
* 이런 식으로 little-endian 순서가 됩니다.
*/
for (n = 0; n < 8U; ++n) { for (n = 0; n < 8U; ++n) {
trailer[n] = (uint8_t)(ctx->length >> (n * 8U)); trailer[n] = (uint8_t)(ctx->length >> (n * 8U));
} }
/*
* 메시지 길이 정보도 인증 상태에 흡수합니다.
* 이렇게 하면 같은 내용이라도 길이 처리가 명확해집니다.
*/
gerbera_auth_update(ctx, trailer, sizeof(trailer)); gerbera_auth_update(ctx, trailer, sizeof(trailer));
/*
* 최종 확산 단계입니다.
*
* 256번 반복하면서 state 내부의 서로 다른 위치들을 다시 섞습니다.
* 32바이트 상태를 8바퀴 도는 효과가 있습니다.
*/
for (n = 0; n < 256U; ++n) { for (n = 0; n < 256U; ++n) {
/*
* 현재 n 기준으로 이웃한 두 state 값을 가져와 더하고,
* n 자체도 더해서 중간값을 만듭니다.
*/
uint8_t value = (uint8_t)(ctx->state[(n + 1U) & 31U] + uint8_t value = (uint8_t)(ctx->state[(n + 1U) & 31U] +
ctx->state[(n + 13U) & 31U] + n); ctx->state[(n + 13U) & 31U] + n);
/*
* value를 n에 따라 0~7비트 회전한 뒤,
* 현재 state 위치에 XOR하여 최종 상태를 더 섞습니다.
*/
ctx->state[n & 31U] ^= rotate_left8(value, n & 7U); ctx->state[n & 31U] ^= rotate_left8(value, n & 7U);
} }
/*
* 내부 state는 32바이트지만,
* 공개할 인증 태그는 GERBERA_TAG_SIZE 바이트입니다.
*
* 보통 여기서는 16바이트 태그를 만든다고 가정합니다.
*/
for (n = 0; n < GERBERA_TAG_SIZE; ++n) { for (n = 0; n < GERBERA_TAG_SIZE; ++n) {
/*
* state 앞쪽 16바이트와 뒤쪽 16바이트를 XOR해서
* 최종 tag 바이트로 압축합니다.
*/
tag[n] = (uint8_t)(ctx->state[n] ^ ctx->state[n + 16U]); 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) int gerbera_constant_time_equal(const uint8_t a[], const uint8_t b[], size_t length)
{ {
/* 배열을 순회할 인덱스입니다. */
size_t n; size_t n;
/*
* 지금까지 발견한 차이를 누적하는 변수입니다.
* 하나라도 다른 바이트가 있으면 0이 아닌 값이 됩니다.
*/
uint8_t difference = 0; uint8_t difference = 0;
/*
* 모든 바이트를 끝까지 비교합니다.
* 중간에 다르다고 바로 return하지 않습니다.
*/
for (n = 0; n < length; ++n) { for (n = 0; n < length; ++n) {
/*
* a[n]과 b[n]이 같으면 XOR 결과는 0입니다.
* 다르면 0이 아닌 값입니다.
*
* OR로 누적하므로, 한 번이라도 차이가 있으면 difference는 0이 아니게 됩니다.
*/
difference |= (uint8_t)(a[n] ^ b[n]); difference |= (uint8_t)(a[n] ^ b[n]);
} }
/*
* difference가 0이면 모든 바이트가 같다는 뜻입니다.
* 같으면 1, 다르면 0을 반환합니다.
*/
return difference == 0; return difference == 0;
} }

View file

@ -1,111 +1,376 @@
#include "gerbera.h" #include "gerbera.h" /* Gerbera 관련 타입, 상수, 함수 선언을 가져옵니다. */
#include <stdio.h> #include <stdio.h> /* FILE, fopen(), fread(), fwrite(), fprintf() 사용 */
#include <string.h> #include <string.h> /* memcmp() 사용 */
/*
* Gerbera 암호문인지 확인하기 위한 고정 파일 서명입니다.
* encrypt_file에서 쓴 magic 값과 반드시 같아야 합니다.
*/
static const uint8_t expected_magic[GERBERA_MAGIC_SIZE] = static const uint8_t expected_magic[GERBERA_MAGIC_SIZE] =
{'M', 'I', 'Z', 'U', 'K', 'I', 0xF0, 0x9F, 0x8E, 0x80}; {'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]) static uint64_t decode_u64_le(const uint8_t bytes[8])
{ {
/* 최종적으로 조립될 64비트 정수입니다. */
uint64_t value = 0; uint64_t value = 0;
/* 0번 바이트부터 7번 바이트까지 순회하기 위한 변수입니다. */
unsigned n; unsigned n;
/* 낮은 바이트부터 높은 바이트까지 순서대로 조립합니다. */
for (n = 0; n < 8U; ++n) { for (n = 0; n < 8U; ++n) {
/*
* bytes[n]을 uint64_t로 확장한 뒤,
* 원래 위치인 n * 8비트만큼 왼쪽으로 이동시킵니다.
*
* OR 연산으로 value에 누적하면 8바이트가 하나의 64비트 값이 됩니다.
*/
value |= (uint64_t)bytes[n] << (n * 8U); value |= (uint64_t)bytes[n] << (n * 8U);
} }
/* 조립된 64비트 값을 반환합니다. */
return value; return value;
} }
/*
* input_path에 있는 Gerbera 암호문 파일을 복호화해서 output_path에 저장합니다.
*
* 반환값:
* 0 = 성공
* 1 = 실패
*/
int decrypt_file(const char *input_path, const char *output_path, int decrypt_file(const char *input_path, const char *output_path,
const char *password) const char *password)
{ {
/* 암호문 입력 파일 포인터입니다. 아직 열지 않았으므로 NULL로 초기화합니다. */
FILE *input = NULL; FILE *input = NULL;
/* 복호화 결과를 쓸 출력 파일 포인터입니다. */
FILE *output = NULL; FILE *output = NULL;
/* 파일에서 읽은 magic 값을 저장합니다. */
uint8_t magic[GERBERA_MAGIC_SIZE]; uint8_t magic[GERBERA_MAGIC_SIZE];
/* 파일에서 읽은 nonce를 저장합니다. */
uint8_t nonce[GERBERA_NONCE_SIZE]; uint8_t nonce[GERBERA_NONCE_SIZE];
/* 파일에서 읽은 원문 크기 필드 8바이트를 저장합니다. */
uint8_t size_bytes[8]; uint8_t size_bytes[8];
/* 암호문을 청크 단위로 읽고 복호화할 임시 버퍼입니다. */
uint8_t buffer[GERBERA_BUFFER_SIZE]; uint8_t buffer[GERBERA_BUFFER_SIZE];
/* 파일 끝에 저장되어 있던 인증 태그입니다. */
uint8_t stored_tag[GERBERA_TAG_SIZE]; uint8_t stored_tag[GERBERA_TAG_SIZE];
/* 현재 입력 파일 내용으로 다시 계산한 인증 태그입니다. */
uint8_t calculated_tag[GERBERA_TAG_SIZE]; uint8_t calculated_tag[GERBERA_TAG_SIZE];
/* 복호화에 사용할 스트림 암호 상태입니다. */
GerberaCipher cipher; GerberaCipher cipher;
/* 인증 태그 검증에 사용할 인증 상태입니다. */
GerberaAuth auth; GerberaAuth auth;
/* 헤더에 기록된 원래 평문 크기입니다. */
uint64_t plain_size; uint64_t plain_size;
/* 아직 처리해야 할 암호문 본문 바이트 수입니다. */
uint64_t remaining; uint64_t remaining;
/* 전체 암호문 파일 크기입니다. ftell()이 long을 반환하므로 long을 사용합니다. */
long file_size; long file_size;
/* fread()로 실제 읽은 바이트 수입니다. */
size_t count; size_t count;
/*
* 함수 결과입니다.
* 기본값을 실패인 1로 두고, 모든 과정이 성공하면 0으로 바꿉니다.
*/
int result = 1; int result = 1;
/*
* 입력 암호문 파일을 바이너리 읽기 모드로 엽니다.
* "rb"는 줄바꿈 변환 없이 파일 바이트를 그대로 읽습니다.
*/
input = fopen(input_path, "rb"); input = fopen(input_path, "rb");
/*
* 파일을 열고,
* 파일 끝으로 이동해서 크기를 구한 뒤,
* 다시 처음으로 되돌립니다.
*
* 하나라도 실패하면 복호화를 진행할 수 없습니다.
*/
if (input == NULL || fseek(input, 0, SEEK_END) != 0 || if (input == NULL || fseek(input, 0, SEEK_END) != 0 ||
(file_size = ftell(input)) < 0 || fseek(input, 0, SEEK_SET) != 0) { (file_size = ftell(input)) < 0 || fseek(input, 0, SEEK_SET) != 0) {
/* 입력 파일을 열 수 없거나 크기를 구할 수 없다는 메시지입니다. */
fprintf(stderr, "Could not open the encrypted file.\n"); fprintf(stderr, "Could not open the encrypted file.\n");
/* 열린 파일이 있으면 닫기 위해 cleanup으로 이동합니다. */
goto cleanup; goto cleanup;
} }
/*
* 파일이 최소한의 Gerbera 파일 크기보다 작은지 확인하고,
* 헤더의 magic, nonce, size_bytes를 차례대로 읽습니다.
*
* 또한 magic이 expected_magic과 같은지 확인합니다.
*/
if (file_size < GERBERA_OVERHEAD_SIZE || if (file_size < GERBERA_OVERHEAD_SIZE ||
fread(magic, 1, sizeof(magic), input) != sizeof(magic) || fread(magic, 1, sizeof(magic), input) != sizeof(magic) ||
fread(nonce, 1, sizeof(nonce), input) != sizeof(nonce) || fread(nonce, 1, sizeof(nonce), input) != sizeof(nonce) ||
fread(size_bytes, 1, sizeof(size_bytes), input) != sizeof(size_bytes) || fread(size_bytes, 1, sizeof(size_bytes), input) != sizeof(size_bytes) ||
memcmp(magic, expected_magic, sizeof(magic)) != 0) { memcmp(magic, expected_magic, sizeof(magic)) != 0) {
/*
* 파일 구조가 너무 작거나,
* 헤더를 제대로 읽지 못했거나,
* magic 값이 다르면 Gerbera 파일이 아니라고 처리합니다.
*/
fprintf(stderr, "The input is not a Gerbera file.\n"); fprintf(stderr, "The input is not a Gerbera file.\n");
/* cleanup으로 이동합니다. */
goto cleanup; goto cleanup;
} }
/*
* 헤더의 8바이트 원문 크기 필드를 uint64_t로 복원합니다.
*/
plain_size = decode_u64_le(size_bytes); 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)) { if (plain_size != (uint64_t)(file_size - GERBERA_OVERHEAD_SIZE)) {
/* 헤더에 기록된 크기와 실제 파일 크기가 맞지 않는 경우입니다. */
fprintf(stderr, "The file size metadata is invalid.\n"); fprintf(stderr, "The file size metadata is invalid.\n");
/* cleanup으로 이동합니다. */
goto cleanup; goto cleanup;
} }
/* First pass: authenticate before any plaintext is written. */ /*
* 첫 번째 통과에서는 복호화를 하지 않고 인증 태그를 먼저 검증합니다.
* 인증에 실패하면 출력 파일을 만들지 않기 위함입니다.
*/
gerbera_auth_init(&auth, password, nonce); gerbera_auth_init(&auth, password, nonce);
/*
* magic 값을 인증 상태에 넣습니다.
* 암호화 때도 같은 순서로 인증했으므로 복호화 때도 순서가 같아야 합니다.
*/
gerbera_auth_update(&auth, magic, sizeof(magic)); gerbera_auth_update(&auth, magic, sizeof(magic));
/*
* nonce 값을 인증 상태에 넣습니다.
* nonce가 바뀌면 키스트림이 바뀌므로 반드시 검증 대상이어야 합니다.
*/
gerbera_auth_update(&auth, nonce, sizeof(nonce)); gerbera_auth_update(&auth, nonce, sizeof(nonce));
/*
* 원문 크기 필드도 인증 상태에 넣습니다.
* size_bytes가 변조되면 태그 검증에 실패해야 합니다.
*/
gerbera_auth_update(&auth, size_bytes, sizeof(size_bytes)); gerbera_auth_update(&auth, size_bytes, sizeof(size_bytes));
/*
* remaining은 앞으로 인증해야 할 암호문 본문 길이입니다.
* 처음에는 전체 원문 크기와 같습니다.
*/
remaining = plain_size; remaining = plain_size;
/*
* 암호문 본문을 끝까지 읽으면서 인증 상태에 넣습니다.
* 여기서는 아직 복호화하지 않습니다.
*/
while (remaining > 0) { while (remaining > 0) {
/*
* 이번에 읽을 바이트 수입니다.
*
* 남은 크기가 buffer보다 작으면 남은 크기만큼,
* 아니면 buffer 전체 크기만큼 읽습니다.
*/
size_t wanted = remaining < sizeof(buffer) ? (size_t)remaining : sizeof(buffer); size_t wanted = remaining < sizeof(buffer) ? (size_t)remaining : sizeof(buffer);
/*
* 암호문 본문에서 wanted 바이트만큼 읽습니다.
*/
count = fread(buffer, 1, wanted, input); count = fread(buffer, 1, wanted, input);
/*
* 정확히 wanted만큼 읽지 못했다면 파일이 중간에 잘렸거나 읽기 오류가 난 것입니다.
*/
if (count != wanted) { if (count != wanted) {
/* 암호문 데이터를 읽지 못했다는 메시지입니다. */
fprintf(stderr, "Could not read the encrypted data.\n"); fprintf(stderr, "Could not read the encrypted data.\n");
/* cleanup으로 이동합니다. */
goto cleanup; goto cleanup;
} }
/*
* 읽은 암호문 바이트를 인증 상태에 넣습니다.
* 암호화할 때도 암호문 기준으로 태그를 만들었으므로 그대로 맞춥니다.
*/
gerbera_auth_update(&auth, buffer, count); gerbera_auth_update(&auth, buffer, count);
/* 처리한 만큼 남은 바이트 수를 줄입니다. */
remaining -= count; remaining -= count;
} }
/*
* 암호문 본문 바로 뒤에는 저장된 인증 태그가 있어야 합니다.
*/
if (fread(stored_tag, 1, sizeof(stored_tag), input) != sizeof(stored_tag)) { if (fread(stored_tag, 1, sizeof(stored_tag), input) != sizeof(stored_tag)) {
/* 태그를 읽지 못하면 파일이 잘렸거나 형식이 깨진 것입니다. */
fprintf(stderr, "The authentication tag is missing.\n"); fprintf(stderr, "The authentication tag is missing.\n");
goto cleanup;
} /* 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");
goto 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; 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); gerbera_cipher_init(&cipher, password, nonce);
/*
* 다시 암호문 본문 전체 길이만큼 복호화해야 하므로
* remaining을 plain_size로 되돌립니다.
*/
remaining = plain_size; remaining = plain_size;
/*
* 인증이 끝난 암호문 본문을 다시 읽으면서 복호화합니다.
*/
while (remaining > 0) { while (remaining > 0) {
/*
* 이번에 읽을 바이트 수입니다.
* 마지막 청크에서는 buffer보다 작을 수 있습니다.
*/
size_t wanted = remaining < sizeof(buffer) ? (size_t)remaining : sizeof(buffer); size_t wanted = remaining < sizeof(buffer) ? (size_t)remaining : sizeof(buffer);
/*
* 암호문 본문에서 wanted 바이트만큼 읽습니다.
*/
count = fread(buffer, 1, wanted, input); count = fread(buffer, 1, wanted, input);
/*
* 정확히 wanted만큼 읽지 못하면 파일 읽기 오류입니다.
* 다만 앞에서 인증을 통과했으므로 보통은 발생하지 않아야 합니다.
*/
if (count != wanted) { if (count != wanted) {
/* 복호화용 데이터를 읽지 못했다는 메시지입니다. */
fprintf(stderr, "Could not read data for decryption.\n"); fprintf(stderr, "Could not read data for decryption.\n");
/* cleanup으로 이동합니다. */
goto cleanup; goto cleanup;
} }
/*
* 스트림 암호를 적용해 암호문을 평문으로 되돌립니다.
* XOR 기반이라 암호화와 같은 함수가 복호화에도 사용됩니다.
*/
gerbera_cipher_apply(&cipher, buffer, count); gerbera_cipher_apply(&cipher, buffer, count);
/*
* 복호화된 평문 청크를 출력 파일에 씁니다.
*/
if (fwrite(buffer, 1, count, output) != count) { if (fwrite(buffer, 1, count, output) != count) {
/* 출력 파일 쓰기 실패 메시지입니다. */
fprintf(stderr, "Could not write the decrypted data.\n"); fprintf(stderr, "Could not write the decrypted data.\n");
/* cleanup으로 이동합니다. */
goto cleanup; goto cleanup;
} }
/* 복호화한 만큼 남은 바이트 수를 줄입니다. */
remaining -= count; remaining -= count;
} }
/*
* 여기까지 왔다면 인증과 복호화, 출력 쓰기가 모두 성공한 것입니다.
*/
result = 0; result = 0;
cleanup: cleanup:
/*
* input이 열려 있다면 닫습니다.
* NULL이면 열리지 않았다는 뜻이므로 닫지 않습니다.
*/
if (input != NULL) fclose(input); if (input != NULL) fclose(input);
/*
* output이 열려 있다면 닫습니다.
* fclose() 실패는 출력 파일이 완전히 기록되지 않았을 수 있으므로 실패 처리합니다.
*/
if (output != NULL && fclose(output) != 0) result = 1; if (output != NULL && fclose(output) != 0) result = 1;
/*
* 최종 결과를 반환합니다.
* 0이면 성공, 1이면 실패입니다.
*/
return result; return result;
} }

View file

@ -1,103 +1,336 @@
#include "gerbera.h" #include "gerbera.h" /* Gerbera 관련 타입, 상수, 함수 선언을 가져옵니다. */
#include <stdio.h> #include <stdio.h> /* FILE, fopen(), fread(), fwrite(), fprintf() 사용 */
#include <stdlib.h> #include <stdlib.h> /* srand(), rand(), remove() 사용 */
#include <time.h> #include <time.h> /* time(), clock() 사용 */
/*
* 암호화된 파일인지 확인하기 위한 고정 파일 서명입니다.
* 복호화할 때 파일 맨 앞이 이 값과 일치하는지 확인할 수 있습니다.
*/
static const uint8_t magic[GERBERA_MAGIC_SIZE] = static const uint8_t magic[GERBERA_MAGIC_SIZE] =
{'M', 'I', 'Z', 'U', 'K', 'I', 0xF0, 0x9F, 0x8E, 0x80}; {'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) static void encode_u64_le(uint8_t bytes[8], uint64_t value)
{ {
/* 0번 바이트부터 7번 바이트까지 순회하기 위한 변수입니다. */
unsigned n; unsigned n;
/* 64비트 값을 8비트씩 잘라서 낮은 바이트부터 저장합니다. */
for (n = 0; n < 8U; ++n) { for (n = 0; n < 8U; ++n) {
/*
* n * 8비트만큼 오른쪽으로 밀면 원하는 바이트가 가장 아래로 옵니다.
* uint8_t로 캐스팅해서 하위 8비트만 저장합니다.
*/
bytes[n] = (uint8_t)(value >> (n * 8U)); bytes[n] = (uint8_t)(value >> (n * 8U));
} }
} }
/*
* nonce를 채웁니다.
*
* nonce는 같은 비밀번호와 같은 파일을 암호화해도
* 매번 다른 암호문이 나오게 만드는 값입니다.
*/
static int random_nonce(uint8_t nonce[GERBERA_NONCE_SIZE]) static int random_nonce(uint8_t nonce[GERBERA_NONCE_SIZE])
{ {
/*
* srand()가 이미 호출되었는지 저장하는 정적 변수입니다.
* static 지역 변수라서 함수 호출이 끝나도 값이 유지됩니다.
*/
static int initialized = 0; static int initialized = 0;
/*
* 난수 생성기 시드는 한 번만 설정합니다.
* 매번 srand()를 다시 호출하면 짧은 시간 안에 같은 시드가 들어갈 수 있습니다.
*/
if (!initialized) { if (!initialized) {
/*
* time(NULL):
* 현재 시간을 초 단위로 반환합니다.
*
* clock():
* 프로그램이 사용한 CPU 시간을 반환합니다.
*
* 두 값을 XOR해서 rand()의 시드로 사용합니다.
*
* 단, rand()는 보안용 난수가 아니므로
* 실제 암호화 프로그램에서는 권장되지 않습니다.
*/
srand((unsigned)time(NULL) ^ (unsigned)clock()); srand((unsigned)time(NULL) ^ (unsigned)clock());
/* 이제 시드 설정이 끝났다고 표시합니다. */
initialized = 1; initialized = 1;
} }
/* nonce 배열의 모든 바이트를 채웁니다. */
for (size_t i = 0; i < GERBERA_NONCE_SIZE; ++i) { for (size_t i = 0; i < GERBERA_NONCE_SIZE; ++i) {
/*
* rand()가 반환한 정수의 하위 8비트만 사용합니다.
* 이 값이 nonce의 한 바이트가 됩니다.
*/
nonce[i] = (uint8_t)rand(); nonce[i] = (uint8_t)rand();
} }
/*
* 현재 구현에서는 실패할 수 있는 시스템 난수 API를 쓰지 않으므로
* 항상 성공을 의미하는 1을 반환합니다.
*/
return 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; FILE *input = NULL;
/* 출력 파일 포인터입니다. 아직 열지 않았으므로 NULL로 초기화합니다. */
FILE *output = NULL; FILE *output = NULL;
/* 파일마다 새로 생성되는 nonce입니다. */
uint8_t nonce[GERBERA_NONCE_SIZE]; uint8_t nonce[GERBERA_NONCE_SIZE];
/* 원본 파일 크기를 little-endian으로 저장할 8바이트 배열입니다. */
uint8_t size_bytes[8]; uint8_t size_bytes[8];
/* 파일을 청크 단위로 읽고 암호화할 임시 버퍼입니다. */
uint8_t buffer[GERBERA_BUFFER_SIZE]; uint8_t buffer[GERBERA_BUFFER_SIZE];
/* 최종 인증 태그를 저장할 배열입니다. */
uint8_t tag[GERBERA_TAG_SIZE]; uint8_t tag[GERBERA_TAG_SIZE];
/* 스트림 암호 상태입니다. */
GerberaCipher cipher; GerberaCipher cipher;
/* 인증 태그 계산 상태입니다. */
GerberaAuth auth; GerberaAuth auth;
/* 입력 파일 크기를 저장합니다. ftell()이 long을 반환하므로 long을 사용합니다. */
long input_size; long input_size;
/* fread()로 실제 읽은 바이트 수를 저장합니다. */
size_t count; size_t count;
/*
* 함수 결과입니다.
* 기본값을 실패인 1로 두고,
* 모든 과정이 성공했을 때만 0으로 바꿉니다.
*/
int result = 1; int result = 1;
/*
* 입력 파일을 바이너리 읽기 모드로 엽니다.
* "rb"를 쓰면 Windows에서도 줄바꿈 변환 없이 원본 바이트 그대로 읽습니다.
*/
input = fopen(input_path, "rb"); input = fopen(input_path, "rb");
/*
* 입력 파일을 열었는지 확인하고,
* 파일 끝으로 이동한 뒤,
* ftell()로 파일 크기를 얻고,
* 다시 파일 처음으로 돌아옵니다.
*
* 하나라도 실패하면 암호화를 진행할 수 없습니다.
*/
if (input == NULL || fseek(input, 0, SEEK_END) != 0 || if (input == NULL || fseek(input, 0, SEEK_END) != 0 ||
(input_size = ftell(input)) < 0 || fseek(input, 0, SEEK_SET) != 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"); fprintf(stderr, "Could not open the input file or determine its size.\n");
goto cleanup;
} /*
if (!random_nonce(nonce)) { * cleanup으로 이동해 열려 있을 수 있는 파일을 닫고 종료합니다.
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");
goto 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); 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) || if (fwrite(magic, 1, sizeof(magic), output) != sizeof(magic) ||
fwrite(nonce, 1, sizeof(nonce), output) != sizeof(nonce) || fwrite(nonce, 1, sizeof(nonce), output) != sizeof(nonce) ||
fwrite(size_bytes, 1, sizeof(size_bytes), output) != sizeof(size_bytes)) { fwrite(size_bytes, 1, sizeof(size_bytes), output) != sizeof(size_bytes)) {
/* 헤더 쓰기에 실패한 경우 오류 메시지를 출력합니다. */
fprintf(stderr, "Could not write the encrypted file header.\n"); fprintf(stderr, "Could not write the encrypted file header.\n");
/* cleanup으로 이동합니다. */
goto cleanup; goto cleanup;
} }
/*
* 스트림 암호 상태를 초기화합니다.
* password와 nonce가 같으면 복호화 때 같은 키스트림이 생성됩니다.
*/
gerbera_cipher_init(&cipher, password, nonce); gerbera_cipher_init(&cipher, password, nonce);
/*
* 인증 태그 계산 상태를 초기화합니다.
* 암호문이 변조되었는지 확인하기 위해 사용됩니다.
*/
gerbera_auth_init(&auth, password, nonce); gerbera_auth_init(&auth, password, nonce);
/*
* magic 값을 인증 대상에 넣습니다.
* 이렇게 하면 파일 서명이 바뀌어도 인증 태그 검증에 실패합니다.
*/
gerbera_auth_update(&auth, magic, sizeof(magic)); gerbera_auth_update(&auth, magic, sizeof(magic));
/*
* nonce도 인증 대상에 넣습니다.
* nonce가 바뀌면 복호화 키스트림이 달라지므로 반드시 보호해야 합니다.
*/
gerbera_auth_update(&auth, nonce, sizeof(nonce)); gerbera_auth_update(&auth, nonce, sizeof(nonce));
/*
* 원본 파일 크기 필드도 인증 대상에 넣습니다.
* size_bytes가 변조되어도 태그 검증에서 잡을 수 있습니다.
*/
gerbera_auth_update(&auth, size_bytes, sizeof(size_bytes)); gerbera_auth_update(&auth, size_bytes, sizeof(size_bytes));
/*
* 입력 파일을 buffer 크기만큼 반복해서 읽습니다.
* 큰 파일도 한 번에 메모리에 올리지 않고 처리할 수 있습니다.
*/
while ((count = fread(buffer, 1, sizeof(buffer), input)) > 0) { while ((count = fread(buffer, 1, sizeof(buffer), input)) > 0) {
/*
* 읽어온 count 바이트를 제자리에서 암호화합니다.
* buffer 안의 평문이 암호문으로 바뀝니다.
*/
gerbera_cipher_apply(&cipher, buffer, count); gerbera_cipher_apply(&cipher, buffer, count);
/*
* 암호화된 바이트를 인증 상태에 넣습니다.
* 즉, 이 구현은 평문이 아니라 암호문을 인증합니다.
*/
gerbera_auth_update(&auth, buffer, count); gerbera_auth_update(&auth, buffer, count);
/*
* 암호화된 청크를 출력 파일에 씁니다.
*/
if (fwrite(buffer, 1, count, output) != count) { if (fwrite(buffer, 1, count, output) != count) {
/* 쓰기 실패 메시지를 출력합니다. */
fprintf(stderr, "Could not write the encrypted data.\n"); fprintf(stderr, "Could not write the encrypted data.\n");
/* cleanup으로 이동합니다. */
goto cleanup; goto cleanup;
} }
} }
/*
* fread() 반복문이 끝난 이유가 정상 EOF인지,
* 아니면 읽기 오류 때문인지 확인합니다.
*/
if (ferror(input)) { if (ferror(input)) {
/* 읽기 오류 메시지를 출력합니다. */
fprintf(stderr, "An error occurred while reading the input file.\n"); fprintf(stderr, "An error occurred while reading the input file.\n");
/* cleanup으로 이동합니다. */
goto cleanup; goto cleanup;
} }
/*
* 지금까지 인증 상태에 넣은 값들로 최종 태그를 만듭니다.
*
* 인증 대상:
* magic
* nonce
* size_bytes
* 암호문 전체
*/
gerbera_auth_finish(&auth, tag); gerbera_auth_finish(&auth, tag);
/*
* 최종 인증 태그를 파일 맨 끝에 붙입니다.
* 복호화할 때 이 태그를 다시 계산해 비교하면 변조 여부를 확인할 수 있습니다.
*/
if (fwrite(tag, 1, sizeof(tag), output) != sizeof(tag)) { if (fwrite(tag, 1, sizeof(tag), output) != sizeof(tag)) {
/* 태그 쓰기 실패 메시지를 출력합니다. */
fprintf(stderr, "Could not write the authentication tag.\n"); fprintf(stderr, "Could not write the authentication tag.\n");
/* cleanup으로 이동합니다. */
goto cleanup; goto cleanup;
} }
/*
* 여기까지 왔다면 암호화, 인증 태그 생성, 파일 쓰기가 모두 성공한 것입니다.
*/
result = 0; result = 0;
cleanup: cleanup:
/*
* input이 열려 있다면 닫습니다.
* NULL이면 아직 열리지 않았다는 뜻이므로 닫지 않습니다.
*/
if (input != NULL) fclose(input); if (input != NULL) fclose(input);
/*
* output이 열려 있다면 닫습니다.
* fclose()가 실패하면 출력 파일이 완전히 저장되지 않았을 수 있으므로 실패 처리합니다.
*/
if (output != NULL && fclose(output) != 0) result = 1; if (output != NULL && fclose(output) != 0) result = 1;
/*
* 작업이 실패했다면 output_path를 삭제합니다.
* 중간까지만 쓰인 파일을 정상 암호문으로 착각하지 않게 하기 위함입니다.
*/
if (result != 0) remove(output_path); if (result != 0) remove(output_path);
/* 최종 결과를 반환합니다. */
return result; return result;
} }

View file

@ -1,10 +1,20 @@
#include "gerbera.h" #include "gerbera.h" /* encrypt_file(), decrypt_file() 선언을 가져옵니다. */
#include <stdio.h> #include <stdio.h> /* printf(), fprintf(), fgets(), stderr, stdin 사용 */
#include <string.h> #include <string.h> /* strcmp(), strcspn() 사용 */
/*
* 프로그램 사용법을 출력합니다.
*
* program:
* argv[0]으로 받은 실행 파일 이름입니다.
*/
static void print_usage(const char *program) static void print_usage(const char *program)
{ {
/*
* 사용법은 일반 출력(stdout)이 아니라 에러 출력(stderr)으로 보냅니다.
* 잘못된 인자 사용은 정상 결과 출력이 아니기 때문입니다.
*/
fprintf(stderr, fprintf(stderr,
"Usage:\n" "Usage:\n"
" %s encrypt <input file> <output file> <password>\n" " %s encrypt <input file> <output file> <password>\n"
@ -12,80 +22,234 @@ static void print_usage(const char *program)
program, program); program, program);
} }
/*
* 명령줄 인자 없이 실행했을 때 사용하는 대화형 모드입니다.
*
* 사용자에게 mode, input, output, password를 직접 입력받습니다.
*
* 반환값:
* 0 = 성공
* 1 = 입력 실패 또는 암호화/복호화 실패
* 2 = 사용법 오류
*/
static int interactive_mode(void) static int interactive_mode(void)
{ {
/*
* 작업 모드를 저장할 버퍼입니다.
* "encrypt" 또는 "decrypt"가 들어갈 수 있습니다.
*/
char mode[16]; char mode[16];
/*
* 입력 파일 경로를 저장할 버퍼입니다.
* fgets()로 읽기 때문에 공백이 포함된 경로도 받을 수 있습니다.
*/
char input[1024]; char input[1024];
/*
* 출력 파일 경로를 저장할 버퍼입니다.
*/
char output[1024]; char output[1024];
/*
* 비밀번호를 저장할 버퍼입니다.
*
* 주의:
* 이 방식은 입력한 비밀번호가 화면에 그대로 보입니다.
* 실제 프로그램에서는 터미널 echo를 끄는 방식을 사용할 수 있습니다.
*/
char password[1024]; char password[1024];
/* 사용자에게 작업 모드를 입력하라고 안내합니다. */
printf("Mode (encrypt/decrypt): "); 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: "); printf("Input file: ");
if (!fgets(input, sizeof(input), stdin))
return 1;
/*
* 입력 파일 경로 한 줄을 읽습니다.
* 실패하면 1을 반환합니다.
*/
if (!fgets(input, sizeof(input), stdin)) return 1;
/* 사용자에게 출력 파일 경로를 입력하라고 안내합니다. */
printf("Output file: "); printf("Output file: ");
if (!fgets(output, sizeof(output), stdin))
return 1;
/*
* 출력 파일 경로 한 줄을 읽습니다.
* 실패하면 1을 반환합니다.
*/
if (!fgets(output, sizeof(output), stdin)) return 1;
/* 사용자에게 비밀번호를 입력하라고 안내합니다. */
printf("Password: "); 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'; mode[strcspn(mode, "\r\n")] = '\0';
/* input 문자열 끝의 줄바꿈을 제거합니다. */
input[strcspn(input, "\r\n")] = '\0'; input[strcspn(input, "\r\n")] = '\0';
/* output 문자열 끝의 줄바꿈을 제거합니다. */
output[strcspn(output, "\r\n")] = '\0'; output[strcspn(output, "\r\n")] = '\0';
/* password 문자열 끝의 줄바꿈을 제거합니다. */
password[strcspn(password, "\r\n")] = '\0'; password[strcspn(password, "\r\n")] = '\0';
/*
* 비밀번호 첫 글자가 널 문자면 빈 문자열입니다.
* 빈 비밀번호는 너무 쉽게 실수할 수 있으므로 거부합니다.
*/
if (password[0] == '\0') { if (password[0] == '\0') {
/* 빈 비밀번호 오류 메시지를 출력합니다. */
fprintf(stderr, "The password cannot be empty.\n"); fprintf(stderr, "The password cannot be empty.\n");
/* 사용법 오류에 가까우므로 2를 반환합니다. */
return 2; return 2;
} }
/*
* 입력 파일 경로와 출력 파일 경로가 완전히 같은지 확인합니다.
* 같은 파일을 읽으면서 동시에 쓰면 원본이 망가질 수 있습니다.
*/
if (strcmp(input, output) == 0) { if (strcmp(input, output) == 0) {
/* 입력과 출력 경로가 같다는 오류 메시지를 출력합니다. */
fprintf(stderr, "The paths of the input and output files cannot be the same.\n"); fprintf(stderr, "The paths of the input and output files cannot be the same.\n");
/* 잘못된 사용이므로 2를 반환합니다. */
return 2; return 2;
} }
/*
* mode가 "encrypt"이면 암호화를 실행합니다.
* encrypt_file()의 반환값을 그대로 반환합니다.
*/
if (strcmp(mode, "encrypt") == 0) if (strcmp(mode, "encrypt") == 0)
return encrypt_file(input, output, password); return encrypt_file(input, output, password);
/*
* mode가 "decrypt"이면 복호화를 실행합니다.
* decrypt_file()의 반환값을 그대로 반환합니다.
*/
if (strcmp(mode, "decrypt") == 0) if (strcmp(mode, "decrypt") == 0)
return decrypt_file(input, output, password); return decrypt_file(input, output, password);
/*
* encrypt도 decrypt도 아니면 알 수 없는 모드입니다.
*/
fprintf(stderr, "Unknown mode. Use 'encrypt' or 'decrypt'.\n"); fprintf(stderr, "Unknown mode. Use 'encrypt' or 'decrypt'.\n");
/* 잘못된 사용이므로 2를 반환합니다. */
return 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[]) int main(int argc, char *argv[])
{ {
/*
* argc가 1이면 프로그램 이름만 있고 추가 인자가 없는 상태입니다.
* 이 경우 대화형 모드로 전환합니다.
*/
if (argc == 1) if (argc == 1)
return interactive_mode(); return interactive_mode();
/*
* 명령줄 모드에서는 총 5개의 인자가 필요합니다.
*
* argv[0] = 프로그램 이름
* argv[1] = encrypt 또는 decrypt
* argv[2] = 입력 파일 경로
* argv[3] = 출력 파일 경로
* argv[4] = 비밀번호
*/
if (argc != 5) { if (argc != 5) {
/* 사용법을 출력합니다. */
print_usage(argv[0]); print_usage(argv[0]);
/* 인자 개수가 잘못되었으므로 2를 반환합니다. */
return 2; return 2;
} }
/*
* argv[4]는 비밀번호입니다.
* 첫 글자가 널 문자면 빈 비밀번호입니다.
*/
if (argv[4][0] == '\0') { if (argv[4][0] == '\0') {
/* 빈 비밀번호 오류 메시지를 출력합니다. */
fprintf(stderr, "The password cannot be empty.\n"); fprintf(stderr, "The password cannot be empty.\n");
/* 잘못된 사용이므로 2를 반환합니다. */
return 2; return 2;
} }
/*
* argv[2]는 입력 파일 경로,
* argv[3]은 출력 파일 경로입니다.
*
* 두 경로 문자열이 같으면 원본 파일을 덮어쓸 위험이 있습니다.
*/
if (strcmp(argv[2], argv[3]) == 0) { if (strcmp(argv[2], argv[3]) == 0) {
/* 입력과 출력 경로가 같다는 오류 메시지를 출력합니다. */
fprintf(stderr, "The paths of the input and output files cannot be the same.\n"); fprintf(stderr, "The paths of the input and output files cannot be the same.\n");
/* 잘못된 사용이므로 2를 반환합니다. */
return 2; return 2;
} }
/*
* 첫 번째 명령 인자가 "encrypt"이면 암호화를 실행합니다.
*
* argv[2] = 입력 파일
* argv[3] = 출력 파일
* argv[4] = 비밀번호
*/
if (strcmp(argv[1], "encrypt") == 0) if (strcmp(argv[1], "encrypt") == 0)
return encrypt_file(argv[2], argv[3], argv[4]); return encrypt_file(argv[2], argv[3], argv[4]);
/*
* 첫 번째 명령 인자가 "decrypt"이면 복호화를 실행합니다.
*
* argv[2] = 입력 파일
* argv[3] = 출력 파일
* argv[4] = 비밀번호
*/
if (strcmp(argv[1], "decrypt") == 0) if (strcmp(argv[1], "decrypt") == 0)
return decrypt_file(argv[2], argv[3], argv[4]); return decrypt_file(argv[2], argv[3], argv[4]);
/*
* encrypt도 decrypt도 아니면 잘못된 명령입니다.
* 사용법을 다시 출력합니다.
*/
print_usage(argv[0]); print_usage(argv[0]);
/* 잘못된 사용이므로 2를 반환합니다. */
return 2; return 2;
} }