
Un motore di crittografia file multi-thread a gigabyte al secondo. Raggiunge un throughput estremo utilizzando una pipeline io_uring triplamente bufferizzata e senza lock, il chunking parallelo di Rayon e AEAD accelerati via hardware (AES-256-GCM / ChaCha20).
Un motore di crittografia AEAD multi-thread costruito in Rust. Cripta e decripta file con una produttività di gigabyte al secondo utilizzando una pipeline io_uring a triplo buffer, l'elaborazione parallela dei chunk tramite Rayon e cifrari ottimizzati in assembly tramite ring.
⚠️ AVVISO: SOFTWARE SPERIMENTALE ⚠️
Questo progetto è estremamente recente e attualmente NON è raccomandato per uso in produzione o per dati critici. Sebbene i primitivi crittografici (AES-256-GCM, ChaCha20-Poly1305 tramite ring) e il formato siano solidi, il codice non è stato sottoposto a audit di sicurezza formali o a test estensivi nel mondo reale. Utilizzo a proprio rischio. Per proteggere dati sensibili, considera l'uso di strumenti collaudati come GnuPG, age o OpenSSL fino a quando questo progetto non maturerà.
ring (ottimizzato in assembly)--memory)seal_in_place_separate_tag / open_in_place tramite ring minimizza l'allocazione nel ciclo caldostd::alloc con allineamento a 4096 byteBenchmark eseguiti con cargo bench (Criterion, 10 campioni per misurazione). La derivazione della chiave è esclusa: i numeri riflettono solo la produttività crittografica pura.
Hardware:
Nota sull'I/O: Criterion scrive file temporanei in /tmp, che su questo sistema è tmpfs (supportato da RAM). Con O_DIRECT, il kernel non può utilizzare DMA asincrono reale su tmpfs, quindi questi numeri riflettono la produttività del cifrario + overhead di io_uring senza il vantaggio del DMA bypass. Su un vero NVMe Gen4, O_DIRECT elimina il doppio buffering della cache di pagina e abilita DMA direttamente nei pool di buffer allineati, il che dovrebbe produrre una produttività significativamente più alta.
| Dimensione file | AES-256-GCM Cripta | ChaCha20 Cripta | AES-256-GCM Decripta | ChaCha20 Decripta |
|---|---|---|---|---|
| 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 |
Sweep della dimensione chunk (AES-256-GCM, file da 64 MiB):
| Dimensione chunk | Produttività |
|---|---|
| 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 |
Il motore utilizza ring (AES-NI / NEON / ARMv8-CE ottimizzati in assembly) per le operazioni di cifratura e una pipeline io_uring a triplo buffer per l'I/O. Tre pool di buffer preallocati ruotano attraverso la pipeline: mentre le scritture del pool A vengono completate nel kernel, il pool B viene crittografato da Rayon sulla CPU e le letture del pool C vengono inviate al kernel. Questo sovrappone la latenza dell'I/O con il calcolo crittografico.
Perché AES-256-GCM è più veloce di ChaCha20-Poly1305 su file piccoli:
Il backend AES-GCM di ring sfrutta le istruzioni hardware AES-NI + CLMUL disponibili su x86-64, dandogli un vantaggio hardware rispetto a ChaCha20 (che è un cifrario software). A dimensioni maggiori, entrambi i cifrari convergono a circa 1.0 GiB/s, indicando che il collo di bottiglia si sposta dalla produttività del cifrario all'overhead di invio dell'I/O.
Perché la produttività di picco è a 1-16 MiB, non a 256 MiB: I file piccoli (1-16 MiB) hanno pochi chunk, quindi il parallelismo di Rayon è efficiente e il working set si adatta alla cache. A 64-256 MiB, la pipeline io_uring è completamente attiva (tre lotti in volo), ma l'overhead per SQE e completamento CQE scala con il numero di chunk. Il design a triplo buffer garantisce che I/O e crittografia si sovrappongano, nascondendo parzialmente questo costo.
Perché circa 1.0 GiB/s e non 10+ GiB/s: L'AES-NI moderno può spingere 2-4 GiB/s per core. Con 12 thread, la produttività grezza del cifrario potrebbe superare 10 GiB/s. Tre fattori spiegano il divario:
PIPELINE_DEPTH=3, solo tre lotti ruotano attraverso la pipeline alla volta. Il vero funzionamento in regime stazionario richiede almeno tre lotti; i file che rientrano in uno o due lotti non beneficiano del pipelining.Ciclo di vita e sicurezza dei buffer:
I pool di buffer vengono allocati una volta tramite std::alloc::alloc_zeroed con Layout::from_size_align(size, 4096) prima che l'anello io_uring venga creato, e vengono riutilizzati in tutte le iterazioni della pipeline senza riallocazione. Ogni chunk crittografato viene riempito di zeri fino all'allineamento del settore prima della scrittura O_DIRECT. L'anello viene esplicitamente rilasciato prima dei pool di buffer, assicurando che il kernel non faccia mai riferimento a memoria liberata (nessun UAF).
git clone https://github.com/frogsnot/concryptor.git
cd concryptor
cargo build --release
Il binario si troverà in target/release/concryptor.
# AES-256-GCM (predefinito), output in myfile.dat.enc
concryptor encrypt myfile.dat
# ChaCha20-Poly1305, percorso output personalizzato
concryptor encrypt myfile.dat --cipher chacha -o encrypted.enc
# Dimensione chunk personalizzata (in MiB)
concryptor encrypt largefile.iso --chunk-size 8
# KDF più forte (costo memoria 512 MiB)
concryptor encrypt secrets.tar --memory 512
# Non interattivo (salta richiesta password)
concryptor encrypt myfile.dat -p "password"
Nota di sicurezza:
--password/-ppassa la password come argomento CLI, visibile nell'output dipse nella cronologia della shell. Per uso interattivo, omettilo per ottenere il prompt nascosto sicuro. Per scripting, preferisci cancellare la cronologia successivamente o utilizzare un wrapper che legga da un descrittore di file.
# Cripta una directory (rilevamento automatico, produce mydir.tar.enc)
concryptor encrypt mydir/
# Con cifrario personalizzato e output
concryptor encrypt mydir/ --cipher chacha -o secrets.enc
La crittografia di directory crea un archivio tar temporaneo (.concryptor-*.tar, permessi 0600, nome CSPRNG), lo cripta, quindi elimina automaticamente il file temporaneo. Nomi file, struttura delle directory, permessi e timestamp sono tutti all'interno del payload crittografato.
# Rimuove automaticamente l'estensione .enc
concryptor decrypt myfile.dat.enc
# Percorso output personalizzato
concryptor decrypt encrypted.enc -o restored.dat
# Non interattivo
concryptor decrypt myfile.dat.enc -p "password"
# Decripta ed estrae in un unico passaggio (rimuove automaticamente .tar.enc -> nome directory)
concryptor decrypt mydir.tar.enc --extract
# Flag breve, directory output personalizzata
concryptor decrypt mydir.tar.enc -x -o restored_dir/
Senza --extract, decriptare un archivio di directory produce il file .tar intermedio, che puoi ispezionare o estrarre manualmente.
concryptor --help
concryptor encrypt --help
concryptor decrypt --help
Tutti i valori sono little-endian. L'intestazione occupa un intero settore da 4 KiB; ogni slot di chunk crittografato viene riempito fino al successivo limite di 4 KiB. Questo garantisce che ogni offset e dimensione di I/O sia allineata al settore per O_DIRECT.
Offset Dimensione Campo
------ ----------- ---------------------
0 10 Byte magici "CONCRYPTOR"
10 1 Versione formato (4)
11 1 Tipo cifrario (0 = AES-256-GCM, 1 = ChaCha20-Poly1305)
12 4 Dimensione chunk (byte, LE)
16 8 Dimensione file originale (byte, LE)
24 16 Salt Argon2 (casuale crittografico, unico per file)
40 12 Base nonce (casuale crittografico, unico per file)
52 4 m_cost Argon2 in KiB (LE, 0 = legacy 64 MiB)
56 4 t_cost / iterazioni Argon2 (LE, 0 = legacy 3)
60 4 p_cost / parallelismo Argon2 (LE, 0 = legacy 4)
64 4032 Riservato (riempito con zeri fino a 4096 byte)
4096 ... [Chunk 0: testo cifrato + tag 16 byte + riempimento zeri fino al limite del settore]
[Chunk 1: testo cifrato + tag 16 byte + riempimento zeri fino al limite del settore]
...
Per chunk da 4 MiB: ogni slot su disco è ceil((4194304 + 16) / 4096) * 4096 = 4198400 byte (4080 byte di riempimento per chunk). I 4032 byte riservati nell'intestazione sono disponibili per funzionalità future (slot di chiave asimmetrica, metadati, ecc.).
Il salt e il base nonce vengono generati freschi da rand::rng() (supportato dal CSPRNG del sistema operativo) ad ogni crittografia. Riutilizzare una password tra file è sicuro perché salt diversi producono chiavi Argon2id diverse e base nonce diversi producono nonce per chunk diversi.
chunk_nonce = base_nonce XOR chunk_index (stile TLS 1.3). Scambiare i chunk causa il fallimento della decifratura perché il nonce alla posizione N non corrisponderà al nonce utilizzato per crittografare il chunk originariamente alla posizione M. Nota: la derivazione del nonce basata su XOR ha una debolezza teorica quando la stessa chiave viene utilizzata su più flussi (base nonce distinti possono produrre spazi di nonce sovrapposti). Questo non si applica a Concryptor perché ogni crittografia genera un salt casuale a 128 bit fresco, producendo una chiave Argon2id unica per file. L'unicità del nonce è rilevante solo sotto la stessa chiave e la probabilità di riutilizzo della chiave è ~2^-128 per coppia di file.AAD = full_aligned_header (4096) || chunk_index (8 LE) || is_final (1) (4105 byte totali). L'intero settore di intestazione da 4 KiB (campi principali, parametri KDF e padding riservato) è vincolato nel tag di autenticazione di ogni chunk. Modificare qualsiasi byte dell'intestazione (tipo cifrario, dimensione chunk, dimensione originale, salt, nonce, parametri KDF o padding riservato) invalida tutti i chunk. Questo previene attacchi di troncamento in cui un avversario modifica original_size e rimuove i chunk finali, e impedisce anche lo smuggling di dati nella regione di padding riservato. I file legacy v3 vengono decifrati con AAD da 52 byte per compatibilità all'indietro; non è possibile alcun downgrade da v4 a v3 perché il byte di versione stesso è all'interno dell'AAD autenticato.0x01 per il chunk finale e 0x00 per tutti gli altri. Questo previene due attacchi:
is_final = 0x00 ma la decifratura si aspetta 0x01.is_final = 0x01 senza la chiave.rand::rng()) per ogni crittografia. Due crittografie dello stesso file con la stessa password producono testo cifrato completamente diverso. Il riutilizzo del nonce (che è catastrofico per AES-GCM) è evitato per costruzione.--memory), 3 iterazioni di tempo, parallelismo 4. Il default di 256 MiB è 4 volte il minimo OWASP ed è costoso per attaccanti GPU/FPGA/ASIC. I parametri KDF sono memorizzati nell'intestazione del file (byte 52-63), rendendo i file auto-descrittivi — la decifratura utilizza sempre i parametri corretti indipendentemente dalle impostazioni predefinite correnti. Se i byte 52-63 sono tutti zero (file legacy pre-parametri-KDF), vengono applicati i vecchi default di 64 MiB / 3 / 4.# Esegui l'intera suite di test (67 test)
cargo test
# Esegui i benchmark (report HTML in target/criterion/)
cargo bench
# Filtra i benchmark
cargo bench -- "encrypt/AES"
cargo bench -- "chunk_sweep"
La suite di test copre:
original_size modificato + chunk rimossi)chunk_size modificato)| Crate | Scopo |
|---|---|
ring | AES-256-GCM e ChaCha20-Poly1305 AEAD ottimizzati in assembly |
io-uring | Interfaccia Linux io_uring per I/O asincrono di lettura/scrittura |
libc | Flag O_DIRECT e pread/pwrite allineati per I/O dell'intestazione |
argon2 | Derivazione chiave Argon2id |
rayon | Elaborazione parallela dei chunk |
clap | Parsing degli argomenti CLI |
indicatif | Barra di progresso nel terminale |
rand | Generazione di numeri casuali crittografici |
zeroize | Cancellazione sicura della memoria |
anyhow | Gestione degli errori |
rpassword | Input password nascosto |
tar | Archiviazione ed estrazione di directory |
# Da crates.io (raccomandato)
cargo install concryptor
# Dal sorgente
git clone https://github.com/FrogSnot/Concryptor
cd Concryptor
cargo build --release
# Il binario si trova in target/release/concryptor
Questo progetto è concesso in licenza con GNU Affero General Public License v3.0.