
초당 기가바이트 처리 속도의 멀티스레드 파일 암호화 엔진입니다. 락프리(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, 64 MiB 파일):
엔진은 암호 연산에 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 키를 생성하고, 다른 기본 논스는 다른 청크당 논스를 생성하기 때문입니다.
chunk_nonce = base_nonce XOR chunk_index (TLS 1.3 스타일). 청크를 바꾸면 복호화에 실패합니다. N 위치의 논스가 M 위치의 청크를 암호화하는 데 사용된 논스와 일치하지 않기 때문입니다. 참고: XOR 기반 논스 유도는 동일한 키가 여러 스트림에서 사용될 때 이론적 약점이 있습니다 (서로 다른 기본 논스가 중복되는 논스 공간을 생성할 수 있음). 이는 각 암호화가 새로운 128비트 무작위 솔트를 생성하여 파일당 고유한 Argon2id 키를 생성하므로 Concryptor에는 적용되지 않습니다. 논스 고유성은 동일한 키에서만 중요하며, 키 재사용 확률은 파일 쌍당 ~2^-128입니다.AAD = full_aligned_header (4096) || chunk_index (8 LE) || is_final (1) (총 4105 바이트)를 사용합니다. 전체 4 KiB 헤더 섹터(코어 필드, KDF 매개변수, 예약 패딩)가 모든 청크의 인증 태그에 바인딩됩니다. 모든 헤더 바이트(암호 유형, 청크 크기, 원본 크기, 솔트, 논스, KDF 매개변수, 예약 패딩)를 수정하면 모든 청크가 무효화됩니다. 이는 공격자가 original_size를 편집하고 후행 청크를 제거하는 잘림 공격을 방지하고, 예약 패딩 영역에 데이터를 밀수하는 것도 방지합니다. 레거시 v3 파일은 이전 버전과의 호환성을 위해 52바이트 AAD로 복호화됩니다. v4에서 v3로의 다운그레이드는 버전 바이트 자체가 인증된 AAD 내에 있으므로 불가능합니다.0x01, 다른 모든 청크의 경우 0x00입니다. 이는 두 가지 공격을 방지합니다:
is_final = 0x00으로 암호화되었지만 복호화는 0x01을 예상하므로 실패합니다.is_final = 0x01에 대한 유효한 태그를 생성할 수 없으므로 실패합니다.rand::rng())에서 추출됩니다. 동일한 패스워드로 동일한 파일을 두 번 암호화하면 완전히 다른 암호문이 생성됩니다. 논스 재사용(AES-GCM에 치명적)은 구조적으로 피해집니다.# 전체 테스트 스위트 실행 (67개 테스트)
cargo test
# 벤치마크 실행 (HTML 보고서는 target/criterion/에)
cargo bench
# 벤치마크 필터링
cargo bench -- "encrypt/AES"
cargo bench -- "chunk_sweep"
테스트 스위트는 다음을 포함합니다:
original_size + 제거된 청크)chunk_size)# crates.io에서 (권장)
cargo install concryptor
# 소스에서
git clone https://github.com/FrogSnot/Concryptor
cd Concryptor
cargo build --release
# 바이너리는 target/release/concryptor에 있습니다.
이 프로젝트는 GNU Affero General Public License v3.0에 따라 라이선스가 부여됩니다.
| 파일 크기 | 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 |
| 청크 크기 | 처리량 |
|---|
| 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 |
--memory로 조정 가능), 3회 시간 반복, 병렬 처리 4. 기본 256 MiB는 OWASP 최소값의 4배이며 GPU/FPGA/ASIC 공격자에게 비용이 많이 듭니다. KDF 매개변수는 파일 헤더(바이트 52-63)에 저장되므로 파일이 자체 기술됩니다 — 현재 기본값과 관계없이 복호화는 항상 올바른 매개변수를 사용합니다. 바이트 52-63이 모두 0이면(레거시 KDF-매개변수 이전 파일) 이전 64 MiB / 3 / 4 기본값이 적용됩니다.| Crate | 용도 |
|---|
ring | 어셈블리 최적화 AES-256-GCM 및 ChaCha20-Poly1305 AEAD |
io-uring | Linux io_uring 인터페이스, 비동기 읽기/쓰기 I/O |
libc | O_DIRECT 플래그 및 헤더 I/O를 위한 정렬된 pread/pwrite |
argon2 | Argon2id 키 유도 |
rayon | 데이터 병렬 청크 처리 |
clap | CLI 인수 파싱 |
indicatif | 터미널 진행률 표시줄 |
rand | 암호학적 난수 생성 |
zeroize | 안전한 메모리 지우기 |
anyhow | 오류 처리 |
rpassword | 숨겨진 패스워드 입력 |
tar | 디렉터리 아카이빙 및 추출 |