
초당 기가바이트 처리 속도의 멀티스레드 파일 암호화 엔진입니다. 락프리(lock-free) 트리플 버퍼링 io_uring 파이프라인, Rayon 병렬 청킹, 하드웨어 가속 AEAD(AES-256-GCM / ChaCha20)를 사용하여 극한의 처리량을 달성합니다.
Rust로 작성된 멀티스레드 AEAD 암호화 엔진입니다. 삼중 버퍼 io_uring 파이프라인, Rayon을 통한 병렬 청크 처리, ring을 통한 어셈블리 최적화 암호를 사용하여 기가바이트/초 처리량으로 파일을 암호화 및 복호화합니다.
⚠️ 면책 조항: 실험적 소프트웨어 ⚠️
이 프로젝트는 매우 초기 단계이며 현재 프로덕션 또는 중요 업무용으로 권장되지 않습니다. 암호화 프리미티브(AES-256-GCM, ring을 통한 ChaCha20-Poly1305)와 형식 설계는 건전하지만, 코드베이스는 공식적인 보안 감사나 광범위한 실제 테스트를 거치지 않았습니다. 사용자는 자신의 책임 하에 사용하십시오. 중요한 데이터를 보호하려면 이 프로젝트가 성숙해질 때까지 GnuPG, age, OpenSSL과 같은 검증된 도구를 사용하는 것이 좋습니다.
ring을 통한 AES-256-GCM (하드웨어 AES-NI) 및 ChaCha20-Poly1305 (어셈블리 최적화)--memory로 설정 가능)ring을 통한 seal_in_place_separate_tag / open_in_place로 핫 루프에서 할당을 최소화합니다.O_DIRECT I/O를 가능하게 하여 NVMe에서 커널 페이지 캐시를 우회하고 DMA 속도의 읽기/쓰기를 제공합니다. 버퍼 풀은 4096바이트 정렬로 std::alloc을 사용합니다.cargo bench (Criterion, 측정당 10개 샘플)로 벤치마크됨. 키 유도는 제외됨 — 수치는 순수 암호 처리량만 반영합니다.
하드웨어:
I/O 참고: Criterion은 /tmp에 임시 파일을 쓰며, 이 시스템에서는 tmpfs (RAM 기반)입니다. O_DIRECT를 사용하면 커널이 tmpfs에서 실제 비동기 DMA를 사용할 수 없으므로, 이 수치는 DMA 우회 이점 없이 암호 처리량 + io_uring 오버헤드를 반영합니다. 실제 Gen4 NVMe 드라이브에서 O_DIRECT는 페이지 캐시 이중 버퍼링을 제거하고 정렬된 버퍼 풀로 직접 DMA를 가능하게 하여 훨씬 더 높은 처리량을 제공해야 합니다.
| 파일 크기 | AES-256-GCM 암호화 | ChaCha20 암호화 | AES-256-GCM 복호화 | ChaCha20 복호화 |
|---|---|---|---|---|
| 64 KiB | 244 MiB/s | 233 MiB/s | 233 MiB/s | 234 MiB/s |
| 1 MiB | 1.08 GiB/s | 882 MiB/s | 1010 MiB/s | 876 MiB/s |
| 16 MiB | 1.10 GiB/s | 923 MiB/s | 1.06 GiB/s | 988 MiB/s |
| 64 MiB | 984 MiB/s | 935 MiB/s | 988 MiB/s | 973 MiB/s |
| 256 MiB | 1.00 GiB/s | 1015 MiB/s | 1.01 GiB/s | 1.02 GiB/s |
청크 크기 스윕 (AES-256-GCM, 64 MiB 파일):
| 청크 크기 | 처리량 |
|---|---|
| 64 KiB | 1.01 GiB/s |
| 256 KiB | 1.05 GiB/s |
| 1 MiB | 1.07 GiB/s |
| 4 MiB | 988 MiB/s |
| 8 MiB | 988 MiB/s |
| 16 MiB | 1.00 GiB/s |
엔진은 암호 연산에 ring (어셈블리 최적화 AES-NI / NEON / ARMv8-CE)을 사용하고 I/O에 삼중 버퍼 io_uring 파이프라인을 사용합니다. 세 개의 미리 할당된 버퍼 풀이 파이프라인을 통해 회전합니다. 풀 A의 쓰기가 커널에서 완료되는 동안, 풀 B는 CPU에서 Rayon에 의해 암호화되고, 풀 C의 읽기는 커널에 제출됩니다. 이는 I/O 지연 시간을 암호화 계산과 오버랩합니다.
작은 파일에서 AES-256-GCM이 ChaCha20-Poly1305보다 빠른 이유:
ring의 AES-GCM 백엔드는 x86-64에서 사용 가능한 AES-NI + CLMUL 하드웨어 명령어를 활용하여 ChaCha20 (소프트웨어 암호)보다 하드웨어 이점을 가집니다. 더 큰 크기에서는 두 암호가 약 1.0 GiB/s로 수렴하여, 병목이 암호 처리량에서 I/O 제출 오버헤드로 이동함을 나타냅니다.
왜 최대 처리량이 256 MiB가 아닌 1-16 MiB에 있는가: 작은 파일(1-16 MiB)은 청크 수가 적어 Rayon 병렬 처리가 효율적이고 작업 세트가 캐시에 맞습니다. 64-256 MiB에서는 io_uring 파이프라인이 완전히 활성화되지만(세 배치가 진행 중), SQE 제출 및 CQE 완료 오버헤드는 청크 수에 따라 증가합니다. 삼중 버퍼 설계는 I/O와 암호화를 오버랩하여 이 비용을 부분적으로 숨깁니다.
왜 ~1.0 GiB/s이고 10+ GiB/s가 아닌가: 최신 AES-NI는 코어당 2-4 GiB/s를 푸시할 수 있습니다. 12개 스레드에서 원시 암호 처리량은 10 GiB/s를 초과할 수 있습니다. 세 가지 요인이 차이를 설명합니다:
PIPELINE_DEPTH=3에서는 한 번에 세 개의 배치만 파이프라인을 통해 회전합니다. 진정한 정상 상태 오버랩을 위해서는 최소한 세 개의 배치가 필요합니다. 한두 개의 배치에 맞는 파일은 파이프라인 효과를 보지 못합니다.버퍼 수명 주기 및 안전성:
버퍼 풀은 io_uring 링이 생성되기 전에 std::alloc::alloc_zeroed와 Layout::from_size_align(size, 4096)을 사용하여 한 번 할당되며, 모든 파이프라인 반복에서 재할당 없이 재사용됩니다. 각 암호화된 청크는 O_DIRECT 쓰기 전에 섹터 정렬로 0으로 패딩됩니다. 링은 버퍼 풀보다 먼저 명시적으로 해제되므로 커널이 해제된 메모리를 참조하지 않습니다 (UAF 없음).
git clone https://github.com/frogsnot/concryptor.git
cd concryptor
cargo build --release
바이너리는 target/release/concryptor에 있습니다.
# AES-256-GCM (기본), 출력 myfile.dat.enc
concryptor encrypt myfile.dat
# ChaCha20-Poly1305, 사용자 정의 출력 경로
concryptor encrypt myfile.dat --cipher chacha -o encrypted.enc
# 사용자 정의 청크 크기 (MiB 단위)
concryptor encrypt largefile.iso --chunk-size 8
# 더 강력한 KDF (512 MiB 메모리 비용)
concryptor encrypt secrets.tar --memory 512
# 비대화형 (암호 프롬프트 건너뛰기)
concryptor encrypt myfile.dat -p "password"
보안 참고:
--password/-p는 패스워드를 CLI 인수로 전달하며, 이는ps출력 및 셸 기록에 표시됩니다. 대화형 사용 시 이를 생략하면 안전한 숨겨진 프롬프트를 얻을 수 있습니다. 스크립팅의 경우, 기록을 나중에 지우거나 파일 디스크립터에서 읽는 래퍼를 사용하는 것이 좋습니다.
# 디렉터리 암호화 (자동 감지, mydir.tar.enc 생성)
concryptor encrypt mydir/
# 사용자 정의 암호 및 출력
concryptor encrypt mydir/ --cipher chacha -o secrets.enc
디렉터리 암호화는 임시 tar 아카이브(.concryptor-*.tar, 0600 권한, CSPRNG 이름)를 생성하고, 이를 암호화한 후 임시 파일을 자동으로 삭제합니다. 파일 이름, 디렉터리 구조, 권한, 타임스탬프는 모두 암호화된 페이로드 내에 있습니다.
# .enc 확장자 자동 제거
concryptor decrypt myfile.dat.enc
# 사용자 정의 출력 경로
concryptor decrypt encrypted.enc -o restored.dat
# 비대화형
concryptor decrypt myfile.dat.enc -p "password"
# 복호화 및 추출을 한 번에 (.tar.enc 자동 제거 -> 디렉터리 이름)
concryptor decrypt mydir.tar.enc --extract
# 짧은 플래그, 사용자 정의 출력 디렉터리
concryptor decrypt mydir.tar.enc -x -o restored_dir/
--extract 없이 디렉터리 아카이브를 복호화하면 중간 .tar 파일이 생성되며, 이를 검사하거나 수동으로 추출할 수 있습니다.
concryptor --help
concryptor encrypt --help
concryptor decrypt --help
모든 값은 리틀 엔디언입니다. 헤더는 전체 4 KiB 섹터를 차지하며, 각 암호화된 청크 슬롯은 다음 4 KiB 경계까지 패딩됩니다. 이렇게 하면 모든 오프셋과 I/O 크기가 O_DIRECT에 대해 섹터 정렬됩니다.
오프셋 크기 필드
------ ----- ---------------------
0 10 매직 바이트 "CONCRYPTOR"
10 1 형식 버전 (4)
11 1 암호 유형 (0 = AES-256-GCM, 1 = ChaCha20-Poly1305)
12 4 청크 크기 (바이트, LE)
16 8 원본 파일 크기 (바이트, LE)
24 16 Argon2 솔트 (암호학적으로 무작위, 파일당 고유)
40 12 기본 논스 (암호학적으로 무작위, 파일당 고유)
52 4 Argon2 m_cost (KiB 단위, LE, 0 = 레거시 64 MiB)
56 4 Argon2 t_cost / 반복 횟수 (LE, 0 = 레거시 3)
60 4 Argon2 p_cost / 병렬 처리 (LE, 0 = 레거시 4)
64 4032 예약됨 (4096 바이트로 0 패딩)
4096 ... [청크 0: 암호문 + 16바이트 태그 + 섹터 경계까지 0 패딩]
[청크 1: 암호문 + 16바이트 태그 + 섹터 경계까지 0 패딩]
...
4 MiB 청크의 경우: 각 디스크 슬롯은 ceil((4194304 + 16) / 4096) * 4096 = 4198400 바이트입니다 (청크당 4080 바이트 패딩). 헤더의 예약된 4032 바이트는 향후 기능(비대칭 키 슬롯, 메타데이터 등)에 사용할 수 있습니다.
솔트와 기본 논스는 매 암호화마다 rand::rng() (OS CSPRNG 지원)에서 새로 생성됩니다. 다른 파일에 대해 패스워드를 재사용하는 것은 안전합니다. 다른 솔트는 다른 Argon2id 키를 생성하고, 다른 기본 논스는 다른 청크당 논스를 생성하기 때문입니다.