init
This commit is contained in:
commit
5625d6bd14
11 changed files with 821 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
tests/*.png
|
||||||
|
tests/*.gerbera
|
||||||
|
build/
|
||||||
|
.vscode
|
||||||
|
.DS_Store
|
||||||
29
Makefile
Normal file
29
Makefile
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
CC ?= cc
|
||||||
|
CPPFLAGS ?= -Iinclude
|
||||||
|
CFLAGS ?= -std=c11 -O2 -Wall -Wextra -Wpedantic
|
||||||
|
LDLIBS ?=
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
208
README.md
Normal file
208
README.md
Normal file
|
|
@ -0,0 +1,208 @@
|
||||||
|
# Gerbera
|
||||||
|
|
||||||
|
배열을 중심으로 직접 설계한 교육용 파일 암호화/복호화 CLI입니다.
|
||||||
|
|
||||||
|
## 빌드와 사용
|
||||||
|
|
||||||
|
macOS 또는 Linux에서는 `make` 없이 다음 스크립트로 빌드합니다.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./scripts/build.sh
|
||||||
|
./build/gerbera encrypt 원본.txt 원본.gerbera '비밀번호'
|
||||||
|
./build/gerbera decrypt 원본.gerbera 복원.txt '비밀번호'
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows에서는 Developer Command Prompt 또는 GCC/Clang이 설정된 터미널에서
|
||||||
|
다음 명령을 사용합니다.
|
||||||
|
|
||||||
|
```bat
|
||||||
|
scripts\build.bat
|
||||||
|
build\gerbera.exe encrypt original.txt original.gerbera password
|
||||||
|
build\gerbera.exe decrypt original.gerbera restored.txt password
|
||||||
|
```
|
||||||
|
|
||||||
|
스크립트 없이 직접 빌드하려면 다음 명령도 사용할 수 있습니다.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cc -Iinclude -std=c11 -O2 src/*.c -o build/gerbera
|
||||||
|
```
|
||||||
|
|
||||||
|
기존 `make`와 `make test`도 선택적으로 계속 사용할 수 있습니다.
|
||||||
|
|
||||||
|
## 프로젝트 구조
|
||||||
|
|
||||||
|
```text
|
||||||
|
Gerbera/
|
||||||
|
├── src/ # 암호화, 복호화, 알고리즘, CLI 소스
|
||||||
|
├── include/gerbera.h # 공통 상수, 자료형, 함수 선언
|
||||||
|
├── scripts/ # make가 필요 없는 빌드 스크립트
|
||||||
|
├── tests/test.sh # 암복호화 자동 테스트
|
||||||
|
├── build/ # 실행 파일과 오브젝트 파일
|
||||||
|
├── Makefile
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
`make clean`을 실행하면 생성물만 담긴 `build/` 디렉터리가 정리됩니다.
|
||||||
|
|
||||||
|
## 난수 생성
|
||||||
|
|
||||||
|
nonce에는 `rand()`를 사용합니다.
|
||||||
|
운영체제 난수를 사용하면 더 좋겠지만 ~~코드가 복잡해집니다.~~ ~~이건 수행평가에요~~
|
||||||
|
|
||||||
|
## 배열이 쓰이는 부분
|
||||||
|
|
||||||
|
- `state[256]`: 바이트 순열 기반 키 스트림 상태
|
||||||
|
- `key[32]`: 비밀번호와 nonce에서 만든 내부 키
|
||||||
|
- `nonce[16]`: 같은 파일/비밀번호라도 결과를 다르게 만드는 난수
|
||||||
|
- `buffer[4096]`: 큰 파일을 일정 크기로 나누어 처리
|
||||||
|
- `tag[16]`: 잘못된 비밀번호와 파일 손상을 검사
|
||||||
|
|
||||||
|
파일 형식은 `MIZUKI🎀(10) + nonce(16) + 원본 크기(8) + 암호문 + tag(16)`입니다.
|
||||||
|
magic의 실제 바이트 배열은 다음과 같습니다.
|
||||||
|
|
||||||
|
```c
|
||||||
|
{'M', 'I', 'Z', 'U', 'K', 'I', 0xF0, 0x9F, 0x8E, 0x80}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gerbera-1 알고리즘 설계
|
||||||
|
|
||||||
|
Gerbera-1은 비밀번호로부터 만든 키 배열과 0부터 255까지의 값을 담은 상태 배열을
|
||||||
|
계속 섞어 키 스트림을 만들고, 이를 파일 데이터와 XOR하는 방식입니다.
|
||||||
|
|
||||||
|
### 기호 정의
|
||||||
|
|
||||||
|
- `P[p]`: 비밀번호의 `p`번째 바이트
|
||||||
|
- `N[n]`: 16바이트 난수 nonce의 `n`번째 바이트
|
||||||
|
- `K[k]`: 32바이트 키 배열
|
||||||
|
- `S[s]`: 256바이트 상태 배열
|
||||||
|
- `ROTL8(x, q)`: 8비트 값 `x`를 왼쪽으로 `q`비트 순환 이동
|
||||||
|
- `⊕`: 비트 단위 XOR
|
||||||
|
|
||||||
|
8비트 순환 이동은 다음과 같이 계산합니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
ROTL8(x, q) = ((x << q) | (x >> (8 - q))) mod 256
|
||||||
|
```
|
||||||
|
|
||||||
|
단, `q = 0`이면 결과는 그대로 `x`입니다.
|
||||||
|
|
||||||
|
### 1. 비밀번호에서 키 배열 생성
|
||||||
|
|
||||||
|
먼저 고정된 32바이트 초기 배열을 `K`에 복사합니다. 암호화 키와 인증 키가
|
||||||
|
같아지지 않도록 첫 번째 원소에 용도 구분 값 `D`를 XOR합니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
K[0] = K[0] ⊕ D
|
||||||
|
D = 0x43 (암호화용)
|
||||||
|
D = 0x41 (인증용)
|
||||||
|
```
|
||||||
|
|
||||||
|
그다음 `r = 0 ... 8191`의 8192라운드 동안 비밀번호의 모든 바이트를 순회합니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
a = (p + r) mod 32
|
||||||
|
b = (a + 11) mod 32
|
||||||
|
c = (a + 23) mod 32
|
||||||
|
|
||||||
|
m = K[b] + P[p] + N[(p + r) mod 16] + r
|
||||||
|
|
||||||
|
K[a] = ROTL8(K[a] ⊕ m, K[c] mod 8) + K[c]
|
||||||
|
```
|
||||||
|
|
||||||
|
각 라운드가 끝날 때 키 배열의 멀리 떨어진 두 원소를 한 번 더 섞습니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
K[r mod 32] = K[r mod 32]
|
||||||
|
⊕ ROTL8(K[(r + 17) mod 32], r mod 8)
|
||||||
|
```
|
||||||
|
|
||||||
|
따라서 같은 비밀번호를 사용해도 파일마다 무작위로 생성되는 `N`이 다르면
|
||||||
|
최종 키 배열도 달라집니다.
|
||||||
|
|
||||||
|
### 2. 256바이트 상태 배열 초기화
|
||||||
|
|
||||||
|
상태 배열은 먼저 `S[n] = n`으로 초기화합니다. 이후 1024회 반복하면서 키와
|
||||||
|
nonce를 이용해 원소의 위치를 교환합니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
S[n] = n, 0 ≤ n < 256
|
||||||
|
j = 0
|
||||||
|
|
||||||
|
for n = 0 ... 1023:
|
||||||
|
i = n mod 256
|
||||||
|
j = j + S[i] + K[n mod 32] + N[n mod 16]
|
||||||
|
swap(S[i], S[j])
|
||||||
|
```
|
||||||
|
|
||||||
|
모든 계산이 바이트 단위이므로 `j` 역시 자동으로 `mod 256`이 적용됩니다.
|
||||||
|
|
||||||
|
### 3. 키 스트림 생성과 암호화
|
||||||
|
|
||||||
|
파일의 각 바이트 위치 `t`마다 상태 배열을 다시 섞고 키 스트림 바이트 `Z[t]`를
|
||||||
|
계산합니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
i = i + 1
|
||||||
|
j = j + S[i] + K[t mod 32]
|
||||||
|
swap(S[i], S[j])
|
||||||
|
|
||||||
|
u = S[(S[i] + S[j]) mod 256]
|
||||||
|
Z[t] = u ⊕ ROTL8(K[(t + u) mod 32], t mod 8)
|
||||||
|
```
|
||||||
|
|
||||||
|
평문 바이트 `M[t]`와 키 스트림을 XOR하면 암호문 `C[t]`가 됩니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
C[t] = M[t] ⊕ Z[t]
|
||||||
|
```
|
||||||
|
|
||||||
|
XOR에는 `x ⊕ y ⊕ y = x`라는 성질이 있으므로 복호화도 같은 수식입니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
M[t] = C[t] ⊕ Z[t]
|
||||||
|
```
|
||||||
|
|
||||||
|
즉, 암호화와 복호화는 같은 키 스트림 생성 함수를 사용하고 입력 데이터만
|
||||||
|
다릅니다. 파일 전체를 메모리에 올리지 않고 `buffer[4096]` 단위로 처리하지만,
|
||||||
|
상태 배열과 현재 위치 `t`를 유지하므로 하나의 연속된 키 스트림이 만들어집니다.
|
||||||
|
|
||||||
|
### 4. 인증 태그 계산
|
||||||
|
|
||||||
|
비밀번호 오류나 파일 변조를 찾기 위해 헤더와 암호문의 각 바이트 `x`를 별도의
|
||||||
|
32바이트 인증 상태 배열 `A`에 누적합니다. 현재까지 처리한 길이를 `L`이라 하면:
|
||||||
|
|
||||||
|
```text
|
||||||
|
a = L mod 32
|
||||||
|
b = (a + 7) mod 32
|
||||||
|
c = (a + 19) mod 32
|
||||||
|
|
||||||
|
A[a] = ROTL8(A[a] ⊕ x ⊕ A[c], A[b] mod 8) + A[b] + a
|
||||||
|
A[c] = A[c] ⊕ ROTL8(x + A[a], a mod 8)
|
||||||
|
L = L + 1
|
||||||
|
```
|
||||||
|
|
||||||
|
모든 데이터를 처리한 뒤 길이 `L`을 8바이트 little-endian 배열로 만들어 같은
|
||||||
|
방식으로 누적합니다. 그 후 256회의 최종 혼합을 수행합니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
for n = 0 ... 255:
|
||||||
|
v = A[(n + 1) mod 32] + A[(n + 13) mod 32] + n
|
||||||
|
A[n mod 32] = A[n mod 32] ⊕ ROTL8(v, n mod 8)
|
||||||
|
```
|
||||||
|
|
||||||
|
마지막 16바이트 인증 태그는 배열의 앞쪽과 뒤쪽을 XOR하여 얻습니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
TAG[n] = A[n] ⊕ A[n + 16], 0 ≤ n < 16
|
||||||
|
```
|
||||||
|
|
||||||
|
복호화할 때는 평문을 출력하기 전에 암호문으로 태그를 다시 계산합니다. 저장된
|
||||||
|
태그와 계산한 태그가 다르면 잘못된 비밀번호 또는 손상된 파일로 판단하여 출력을
|
||||||
|
만들지 않습니다.
|
||||||
|
|
||||||
|
### 전체 처리 과정
|
||||||
|
|
||||||
|
```text
|
||||||
|
암호화: 파일 → 키 스트림 생성 → 원본 바이트 XOR → 암호문 + TAG
|
||||||
|
복호화: 암호문 → TAG 검증 → 같은 키 스트림 XOR → 원본 파일
|
||||||
|
```
|
||||||
43
include/gerbera.h
Normal file
43
include/gerbera.h
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#ifndef GERBERA_H
|
||||||
|
#define GERBERA_H
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define GERBERA_MAGIC_SIZE 10
|
||||||
|
#define GERBERA_NONCE_SIZE 16
|
||||||
|
#define GERBERA_TAG_SIZE 16
|
||||||
|
#define GERBERA_BUFFER_SIZE 4096
|
||||||
|
#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];
|
||||||
|
uint8_t i;
|
||||||
|
uint8_t j;
|
||||||
|
uint64_t position;
|
||||||
|
} GerberaCipher;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t state[32];
|
||||||
|
uint64_t length;
|
||||||
|
} GerberaAuth;
|
||||||
|
|
||||||
|
void gerbera_cipher_init(GerberaCipher *ctx, const char *password,
|
||||||
|
const uint8_t nonce[GERBERA_NONCE_SIZE]);
|
||||||
|
void gerbera_cipher_apply(GerberaCipher *ctx, uint8_t data[], size_t length);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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,
|
||||||
|
const char *password);
|
||||||
|
|
||||||
|
#endif
|
||||||
43
scripts/build.bat
Normal file
43
scripts/build.bat
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
if not exist build mkdir build
|
||||||
|
|
||||||
|
where cl >nul 2>nul
|
||||||
|
if %errorlevel% equ 0 goto build_msvc
|
||||||
|
|
||||||
|
where gcc >nul 2>nul
|
||||||
|
if %errorlevel% equ 0 goto build_gcc
|
||||||
|
|
||||||
|
where clang >nul 2>nul
|
||||||
|
if %errorlevel% equ 0 goto build_clang
|
||||||
|
|
||||||
|
echo C compiler not found. Install Visual Studio Build Tools, GCC, or Clang.
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:build_msvc
|
||||||
|
pushd build
|
||||||
|
cl /nologo /std:c11 /O2 /W4 /I..\include ^
|
||||||
|
..\src\main.c ..\src\encrypt.c ..\src\decrypt.c ..\src\cipher.c ^
|
||||||
|
/Fe:gerbera.exe bcrypt.lib
|
||||||
|
set "result=%errorlevel%"
|
||||||
|
popd
|
||||||
|
if not %result% equ 0 exit /b %result%
|
||||||
|
goto success
|
||||||
|
|
||||||
|
:build_gcc
|
||||||
|
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
|
||||||
|
if not %errorlevel% equ 0 exit /b %errorlevel%
|
||||||
|
goto success
|
||||||
|
|
||||||
|
:build_clang
|
||||||
|
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
|
||||||
|
echo Build complete: build\gerbera.exe
|
||||||
|
exit /b 0
|
||||||
20
scripts/build.sh
Executable file
20
scripts/build.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
compiler="${CC:-cc}"
|
||||||
|
|
||||||
|
mkdir -p build
|
||||||
|
"$compiler" \
|
||||||
|
-Iinclude \
|
||||||
|
-std=c11 \
|
||||||
|
-O2 \
|
||||||
|
-Wall \
|
||||||
|
-Wextra \
|
||||||
|
-Wpedantic \
|
||||||
|
src/main.c \
|
||||||
|
src/encrypt.c \
|
||||||
|
src/decrypt.c \
|
||||||
|
src/cipher.c \
|
||||||
|
-o build/gerbera
|
||||||
|
|
||||||
|
echo "Build complete: build/gerbera"
|
||||||
145
src/cipher.c
Normal file
145
src/cipher.c
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
#include "gerbera.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static uint8_t rotate_left8(uint8_t value, unsigned count)
|
||||||
|
{
|
||||||
|
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
|
||||||
|
* educational, original construction; it is not a replacement for a
|
||||||
|
* reviewed password KDF such as Argon2.
|
||||||
|
*/
|
||||||
|
static void derive_key(uint8_t key[32], const char *password,
|
||||||
|
const uint8_t nonce[GERBERA_NONCE_SIZE], uint8_t domain)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
memcpy(key, initial, sizeof(initial));
|
||||||
|
key[0] ^= domain;
|
||||||
|
|
||||||
|
for (round = 0; round < 8192U; ++round) {
|
||||||
|
for (p = 0; p < password_length; ++p) {
|
||||||
|
size_t a = (p + round) & 31U;
|
||||||
|
size_t b = (a + 11U) & 31U;
|
||||||
|
size_t c = (a + 23U) & 31U;
|
||||||
|
uint8_t mixed = (uint8_t)(key[b] + (uint8_t)password[p] +
|
||||||
|
nonce[(p + round) & 15U] +
|
||||||
|
(uint8_t)round);
|
||||||
|
key[a] = (uint8_t)(rotate_left8((uint8_t)(key[a] ^ mixed),
|
||||||
|
(unsigned)(key[c] & 7U)) + key[c]);
|
||||||
|
}
|
||||||
|
key[round & 31U] ^= rotate_left8(key[(round + 17U) & 31U], round & 7U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void gerbera_cipher_init(GerberaCipher *ctx, const char *password,
|
||||||
|
const uint8_t nonce[GERBERA_NONCE_SIZE])
|
||||||
|
{
|
||||||
|
unsigned n;
|
||||||
|
uint8_t j = 0;
|
||||||
|
|
||||||
|
derive_key(ctx->key, password, nonce, 0x43U);
|
||||||
|
for (n = 0; n < 256U; ++n) {
|
||||||
|
ctx->state[n] = (uint8_t)n;
|
||||||
|
}
|
||||||
|
for (n = 0; n < 1024U; ++n) {
|
||||||
|
uint8_t i = (uint8_t)n;
|
||||||
|
uint8_t temporary;
|
||||||
|
j = (uint8_t)(j + ctx->state[i] + ctx->key[n & 31U] +
|
||||||
|
nonce[n & 15U]);
|
||||||
|
temporary = ctx->state[i];
|
||||||
|
ctx->state[i] = ctx->state[j];
|
||||||
|
ctx->state[j] = temporary;
|
||||||
|
}
|
||||||
|
ctx->i = 0;
|
||||||
|
ctx->j = j;
|
||||||
|
ctx->position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void gerbera_cipher_apply(GerberaCipher *ctx, uint8_t data[], size_t length)
|
||||||
|
{
|
||||||
|
size_t n;
|
||||||
|
|
||||||
|
for (n = 0; n < length; ++n) {
|
||||||
|
uint8_t temporary;
|
||||||
|
uint8_t stream;
|
||||||
|
ctx->i = (uint8_t)(ctx->i + 1U);
|
||||||
|
ctx->j = (uint8_t)(ctx->j + ctx->state[ctx->i] +
|
||||||
|
ctx->key[ctx->position & 31U]);
|
||||||
|
temporary = ctx->state[ctx->i];
|
||||||
|
ctx->state[ctx->i] = ctx->state[ctx->j];
|
||||||
|
ctx->state[ctx->j] = temporary;
|
||||||
|
stream = ctx->state[(uint8_t)(ctx->state[ctx->i] +
|
||||||
|
ctx->state[ctx->j])];
|
||||||
|
stream ^= rotate_left8(ctx->key[(ctx->position + stream) & 31U],
|
||||||
|
(unsigned)(ctx->position & 7U));
|
||||||
|
data[n] ^= stream;
|
||||||
|
++ctx->position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void gerbera_auth_init(GerberaAuth *ctx, const char *password,
|
||||||
|
const uint8_t nonce[GERBERA_NONCE_SIZE])
|
||||||
|
{
|
||||||
|
derive_key(ctx->state, password, nonce, 0x41U);
|
||||||
|
ctx->length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void gerbera_auth_update(GerberaAuth *ctx, const uint8_t data[], size_t length)
|
||||||
|
{
|
||||||
|
size_t n;
|
||||||
|
|
||||||
|
for (n = 0; n < length; ++n) {
|
||||||
|
size_t a = (size_t)(ctx->length & 31U);
|
||||||
|
size_t b = (a + 7U) & 31U;
|
||||||
|
size_t c = (a + 19U) & 31U;
|
||||||
|
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);
|
||||||
|
ctx->state[c] ^= rotate_left8((uint8_t)(data[n] + ctx->state[a]),
|
||||||
|
(unsigned)(a & 7U));
|
||||||
|
++ctx->length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void gerbera_auth_finish(GerberaAuth *ctx, uint8_t tag[GERBERA_TAG_SIZE])
|
||||||
|
{
|
||||||
|
uint8_t trailer[8];
|
||||||
|
unsigned n;
|
||||||
|
|
||||||
|
for (n = 0; n < 8U; ++n) {
|
||||||
|
trailer[n] = (uint8_t)(ctx->length >> (n * 8U));
|
||||||
|
}
|
||||||
|
gerbera_auth_update(ctx, trailer, sizeof(trailer));
|
||||||
|
for (n = 0; n < 256U; ++n) {
|
||||||
|
uint8_t value = (uint8_t)(ctx->state[(n + 1U) & 31U] +
|
||||||
|
ctx->state[(n + 13U) & 31U] + n);
|
||||||
|
ctx->state[n & 31U] ^= rotate_left8(value, n & 7U);
|
||||||
|
}
|
||||||
|
for (n = 0; n < GERBERA_TAG_SIZE; ++n) {
|
||||||
|
tag[n] = (uint8_t)(ctx->state[n] ^ ctx->state[n + 16U]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int gerbera_constant_time_equal(const uint8_t a[], const uint8_t b[], size_t length)
|
||||||
|
{
|
||||||
|
size_t n;
|
||||||
|
uint8_t difference = 0;
|
||||||
|
|
||||||
|
for (n = 0; n < length; ++n) {
|
||||||
|
difference |= (uint8_t)(a[n] ^ b[n]);
|
||||||
|
}
|
||||||
|
return difference == 0;
|
||||||
|
}
|
||||||
112
src/decrypt.c
Normal file
112
src/decrypt.c
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
#include "gerbera.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const uint8_t expected_magic[GERBERA_MAGIC_SIZE] =
|
||||||
|
{'M', 'I', 'Z', 'U', 'K', 'I', 0xF0, 0x9F, 0x8E, 0x80};
|
||||||
|
|
||||||
|
static uint64_t decode_u64_le(const uint8_t bytes[8])
|
||||||
|
{
|
||||||
|
uint64_t value = 0;
|
||||||
|
unsigned n;
|
||||||
|
for (n = 0; n < 8U; ++n) {
|
||||||
|
value |= (uint64_t)bytes[n] << (n * 8U);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
int decrypt_file(const char *input_path, const char *output_path,
|
||||||
|
const char *password)
|
||||||
|
{
|
||||||
|
FILE *input = NULL;
|
||||||
|
FILE *output = NULL;
|
||||||
|
uint8_t magic[GERBERA_MAGIC_SIZE];
|
||||||
|
uint8_t nonce[GERBERA_NONCE_SIZE];
|
||||||
|
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;
|
||||||
|
long file_size;
|
||||||
|
size_t count;
|
||||||
|
int result = 1;
|
||||||
|
|
||||||
|
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");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
fprintf(stderr, "The input is not a Gerbera file.\n");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
plain_size = decode_u64_le(size_bytes);
|
||||||
|
if (plain_size != (uint64_t)(file_size - GERBERA_OVERHEAD_SIZE)) {
|
||||||
|
fprintf(stderr, "The file size metadata is invalid.\n");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* First pass: authenticate before any plaintext is written. */
|
||||||
|
gerbera_auth_init(&auth, password, nonce);
|
||||||
|
gerbera_auth_update(&auth, magic, sizeof(magic));
|
||||||
|
gerbera_auth_update(&auth, nonce, sizeof(nonce));
|
||||||
|
gerbera_auth_update(&auth, size_bytes, sizeof(size_bytes));
|
||||||
|
remaining = plain_size;
|
||||||
|
while (remaining > 0) {
|
||||||
|
size_t wanted = remaining < sizeof(buffer) ? (size_t)remaining : sizeof(buffer);
|
||||||
|
count = fread(buffer, 1, wanted, input);
|
||||||
|
if (count != wanted) {
|
||||||
|
fprintf(stderr, "Could not read the encrypted data.\n");
|
||||||
|
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");
|
||||||
|
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");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
gerbera_cipher_init(&cipher, password, nonce);
|
||||||
|
remaining = plain_size;
|
||||||
|
while (remaining > 0) {
|
||||||
|
size_t wanted = remaining < sizeof(buffer) ? (size_t)remaining : sizeof(buffer);
|
||||||
|
count = fread(buffer, 1, wanted, input);
|
||||||
|
if (count != wanted) {
|
||||||
|
fprintf(stderr, "Could not read data for decryption.\n");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
gerbera_cipher_apply(&cipher, buffer, count);
|
||||||
|
if (fwrite(buffer, 1, count, output) != count) {
|
||||||
|
fprintf(stderr, "Could not write the decrypted data.\n");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
remaining -= count;
|
||||||
|
}
|
||||||
|
result = 0;
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
if (input != NULL) fclose(input);
|
||||||
|
if (output != NULL && fclose(output) != 0) result = 1;
|
||||||
|
if (result != 0) remove(output_path);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
105
src/encrypt.c
Normal file
105
src/encrypt.c
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
#include "gerbera.h"
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
static const uint8_t magic[GERBERA_MAGIC_SIZE] =
|
||||||
|
{'M', 'I', 'Z', 'U', 'K', 'I', 0xF0, 0x9F, 0x8E, 0x80};
|
||||||
|
|
||||||
|
static void encode_u64_le(uint8_t bytes[8], uint64_t value)
|
||||||
|
{
|
||||||
|
unsigned n;
|
||||||
|
for (n = 0; n < 8U; ++n) {
|
||||||
|
bytes[n] = (uint8_t)(value >> (n * 8U));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int random_nonce(uint8_t nonce[GERBERA_NONCE_SIZE])
|
||||||
|
{
|
||||||
|
static int initialized = 0;
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
srand((unsigned)time(NULL));
|
||||||
|
initialized = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < GERBERA_NONCE_SIZE; ++i) {
|
||||||
|
nonce[i] = (uint8_t)rand();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int encrypt_file(const char *input_path, const char *output_path,
|
||||||
|
const char *password)
|
||||||
|
{
|
||||||
|
FILE *input = NULL;
|
||||||
|
FILE *output = NULL;
|
||||||
|
uint8_t nonce[GERBERA_NONCE_SIZE];
|
||||||
|
uint8_t size_bytes[8];
|
||||||
|
uint8_t buffer[GERBERA_BUFFER_SIZE];
|
||||||
|
uint8_t tag[GERBERA_TAG_SIZE];
|
||||||
|
GerberaCipher cipher;
|
||||||
|
GerberaAuth auth;
|
||||||
|
long input_size;
|
||||||
|
size_t count;
|
||||||
|
int result = 1;
|
||||||
|
|
||||||
|
input = fopen(input_path, "rb");
|
||||||
|
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");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
encode_u64_le(size_bytes, (uint64_t)input_size);
|
||||||
|
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");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
gerbera_cipher_init(&cipher, password, nonce);
|
||||||
|
gerbera_auth_init(&auth, password, nonce);
|
||||||
|
gerbera_auth_update(&auth, magic, sizeof(magic));
|
||||||
|
gerbera_auth_update(&auth, nonce, sizeof(nonce));
|
||||||
|
gerbera_auth_update(&auth, size_bytes, sizeof(size_bytes));
|
||||||
|
|
||||||
|
while ((count = fread(buffer, 1, sizeof(buffer), input)) > 0) {
|
||||||
|
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");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ferror(input)) {
|
||||||
|
fprintf(stderr, "An error occurred while reading the input file.\n");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
gerbera_auth_finish(&auth, tag);
|
||||||
|
if (fwrite(tag, 1, sizeof(tag), output) != sizeof(tag)) {
|
||||||
|
fprintf(stderr, "Could not write the authentication tag.\n");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
result = 0;
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
if (input != NULL) fclose(input);
|
||||||
|
if (output != NULL && fclose(output) != 0) result = 1;
|
||||||
|
if (result != 0) remove(output_path);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
91
src/main.c
Normal file
91
src/main.c
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
#include "gerbera.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static void print_usage(const char *program)
|
||||||
|
{
|
||||||
|
fprintf(stderr,
|
||||||
|
"Usage:\n"
|
||||||
|
" %s encrypt <input file> <output file> <password>\n"
|
||||||
|
" %s decrypt <input file> <output file> <password>\n",
|
||||||
|
program, program);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int interactive_mode(void)
|
||||||
|
{
|
||||||
|
char mode[16];
|
||||||
|
char input[1024];
|
||||||
|
char output[1024];
|
||||||
|
char password[1024];
|
||||||
|
|
||||||
|
printf("Mode (encrypt/decrypt): ");
|
||||||
|
if (!fgets(mode, sizeof(mode), stdin))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
printf("Input file: ");
|
||||||
|
if (!fgets(input, sizeof(input), stdin))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
printf("Output file: ");
|
||||||
|
if (!fgets(output, sizeof(output), stdin))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
printf("Password: ");
|
||||||
|
if (!fgets(password, sizeof(password), stdin))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
mode[strcspn(mode, "\r\n")] = '\0';
|
||||||
|
input[strcspn(input, "\r\n")] = '\0';
|
||||||
|
output[strcspn(output, "\r\n")] = '\0';
|
||||||
|
password[strcspn(password, "\r\n")] = '\0';
|
||||||
|
|
||||||
|
if (password[0] == '\0') {
|
||||||
|
fprintf(stderr, "The password cannot be empty.\n");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(input, output) == 0) {
|
||||||
|
fprintf(stderr, "The paths of the input and output files cannot be the same.\n");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(mode, "encrypt") == 0)
|
||||||
|
return encrypt_file(input, output, password);
|
||||||
|
|
||||||
|
if (strcmp(mode, "decrypt") == 0)
|
||||||
|
return decrypt_file(input, output, password);
|
||||||
|
|
||||||
|
fprintf(stderr, "Unknown mode. Use 'encrypt' or 'decrypt'.\n");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
if (argc == 1)
|
||||||
|
return interactive_mode();
|
||||||
|
|
||||||
|
if (argc != 5) {
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (argv[4][0] == '\0') {
|
||||||
|
fprintf(stderr, "The password cannot be empty.\n");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(argv[2], argv[3]) == 0) {
|
||||||
|
fprintf(stderr, "The paths of the input and output files cannot be the same.\n");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strcmp(argv[1], "encrypt") == 0)
|
||||||
|
return encrypt_file(argv[2], argv[3], argv[4]);
|
||||||
|
|
||||||
|
if (strcmp(argv[1], "decrypt") == 0)
|
||||||
|
return decrypt_file(argv[2], argv[3], argv[4]);
|
||||||
|
|
||||||
|
print_usage(argv[0]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
20
tests/test.sh
Normal file
20
tests/test.sh
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
bin="${GERBERA_BIN:-./build/gerbera}"
|
||||||
|
work="${TMPDIR:-/tmp}/gerbera-test-$$"
|
||||||
|
mkdir "$work"
|
||||||
|
trap 'rm -rf "$work"' EXIT HUP INT TERM
|
||||||
|
|
||||||
|
printf 'Array-based encryption test\n\000\001\377' > "$work/plain.bin"
|
||||||
|
"$bin" encrypt "$work/plain.bin" "$work/encrypted.gerbera" 'test-password'
|
||||||
|
"$bin" decrypt "$work/encrypted.gerbera" "$work/decrypted.bin" 'test-password'
|
||||||
|
cmp "$work/plain.bin" "$work/decrypted.bin"
|
||||||
|
|
||||||
|
if "$bin" decrypt "$work/encrypted.gerbera" "$work/wrong.bin" 'wrong-password'; then
|
||||||
|
echo "wrong password unexpectedly succeeded" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
test ! -e "$work/wrong.bin"
|
||||||
|
|
||||||
|
echo "All tests passed."
|
||||||
Loading…
Add table
Add a link
Reference in a new issue