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
build/
.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
.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
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/<name>.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

View file

@ -1,16 +1,23 @@
#ifndef GERBERA_H
#define GERBERA_H
/* 명령줄 진입점과 각 모듈이 함께 쓰는 공개 선언입니다. */
#include <stddef.h>
#include <stdint.h>
/* 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,

View file

@ -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

View file

@ -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"

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)
{
/* 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,
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;
}

View file

@ -1,111 +1,376 @@
#include "gerbera.h"
#include "gerbera.h" /* Gerbera 관련 타입, 상수, 함수 선언을 가져옵니다. */
#include <stdio.h>
#include <string.h>
#include <stdio.h> /* FILE, fopen(), fread(), fwrite(), fprintf() 사용 */
#include <string.h> /* 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;
}

View file

@ -1,103 +1,336 @@
#include "gerbera.h"
#include "gerbera.h" /* Gerbera 관련 타입, 상수, 함수 선언을 가져옵니다. */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdio.h> /* FILE, fopen(), fread(), fwrite(), fprintf() 사용 */
#include <stdlib.h> /* srand(), rand(), remove() 사용 */
#include <time.h> /* 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;
}

View file

@ -1,10 +1,20 @@
#include "gerbera.h"
#include "gerbera.h" /* encrypt_file(), decrypt_file() 선언을 가져옵니다. */
#include <stdio.h>
#include <string.h>
#include <stdio.h> /* printf(), fprintf(), fgets(), stderr, stdin 사용 */
#include <string.h> /* strcmp(), strcspn() 사용 */
/*
* .
*
* program:
* argv[0] .
*/
static void print_usage(const char *program)
{
/*
* (stdout) (stderr) .
* .
*/
fprintf(stderr,
"Usage:\n"
" %s encrypt <input file> <output file> <password>\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;
}