206 lines
6.2 KiB
Markdown
206 lines
6.2 KiB
Markdown
# Gerbera
|
|
|
|
## 빌드와 사용
|
|
|
|
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 → 원본 파일
|
|
```
|