CryptoPipe a fast and secure stream-encryption-utility.
Features:
- Uses Argon2i v1.3 as PBKDF (instead of the outdated PBKDF2)
- Authenticated encryption using ChaCha20+Poly1305 (which is pretty fast even on platforms without AES-acceleration)
- The encrypted-stream-format also allows random access opening/sealing (the data is splitted into ordered 1MiB-chunks which can be processed independently)
- Protects against tampering, data-reordering and -truncation
- Flexible stream-header-format that can be easily extended in the future
- Written in safe Rust (except the bindings to libsodium)
Make sure, you have a working and up-to-date Rust-toolchain installed 😉
To build the documentation go into the project's root-directory and run cargo doc --release
; to open the documentation
in your web-browser run cargo doc --open
.
To build the application, go into the project's root-directory and run cargo build --release
; you can find the build
in target/release.
(A note on terminology: instead of "encrypt"/"decrypt" we use the terms "seal"/"open" because this also implies auth-tag-generation/-validation)
There are three kinds of cryptographic algorithms required:
- A PBKDF to derive a master-key from a password:
pbkdf(key: String) -> Key
- A KDF to derive individual, position-dependent keys for each chunk:
kdf(master_key: Key, info: Bytes) -> Key
- An AuthEnc-scheme (authenticated-encryption-scheme) used to seal each chunk:
auth_enc_seal(key: Key, data: Bytes) -> Bytes
auth_enc_open(key: Key, data: Bytes) -> Result<Bytes, Error>
The master-key-generation is pretty straight forward: we just throw the user's password into the PBKDF to derive
master_key
. For specified PBKDF-algorithms and their parameters see Appendix A.
It is important that each chunk has a unique key because key-reuse might lead to catastrophic failures (including but not limited to the complete loss of secrecy and/or authenticity).
To achieve this property, we use the chunk's index and it's stream-position (read: is the chunk the last one or not):
struct {
u64 chunk_index; // (=> Raw-serialized as 64-bit-big-endian-integer)
char* stream_position; // Either "#Last Chunk" if the chunk is the last one or "" if the chunk is a normal chunk (=> raw-serialized as US-ASCII-string)
} kdf_info; // (=> Raw-serialized without memory-alignment-padding)
Using the chunk-index as KDF-info has two advantages:
- It avoids the need for a unique random per-chunk-nonce (which we would need to save along with the chunk)
- It protects against chunk-reordering: If you swap
chunk_0
andchunk_1
, the chunk-key for the 0th chunk (nowchunk_1
) is still derived with chunk-index 0 and thus cannot openchunk_1
.
For the same reason as in 2nd, we also append the US-ASCII-string #Last Chunk
if the chunk is the last-one so that an
attacker cannot strip it from the file – the key derived for the last-chunk cannot open a normal chunk (and because the
last-chunk's key also depends on it's chunk-index, it cannot be reordered).
The chunk-encryption is also pretty straight forward:
- The user-data is splitted into 1MiB-large chunks (the last chunk may be smaller)
- The chunks are sealed with their unique key (see Per-Chunk Key-Derivation)
The stream consists of two parts:
- The stream-header
- One or more sealed data-chunks
The stream-header and each data-chunk are simply concatenated together (stream_header || chunk_0 || ... || chunk_n
).
The stream-header is consists of an ASN.1-DER-serialized structure which looks like this
struct {
char* magic_number; // This must ALWAYS be the first field to allow testing for compatibility (=> ASN.1-DER-UTF8String)
struct {
char* algorithm; // The PBKDF-algorithm (=> ASN.1-DER-UTF8String)
void* parameters; // Additional algorithm-parameters; see appendix A (=> ASN.1-DER-Struct)
} pbkdf; // (=> ASN.1-DER-Struct)
struct {
char* algorithm; // The KDF-algorithm (=> ASN.1-DER-UTF8String)
void* parameters; // Additional algorithm-parameters; see appendix A (=> ASN.1-DER-Struct)
} kdf; // (=> ASN.1-DER-Struct)
struct {
char* algorithm; // The authenticated-encryption-algorithm (=> ASN.1-DER-UTF8String)
void* parameters; // Additional algorithm-parameters; see appendix A (=> ASN.1-DER-Struct)
} auth_enc; // (=> ASN.1-DER-Struct)
} header_v1; // (=> ASN.1-DER-Struct)
- current:
de.KizzyCode.CryptoPipe.v1
A chunk is simply the authenticated ciphertext (see Chunk-Encryption)
These PBKDFs are currently specified:
The standard Argon2i v1.3-algorithm.
- algorithm:
Argon2i@v1.3
- parameters:
struct { u8* nonce; // The PBKDF-nonce (=> ASN.1-DER-OctetString) u32 time_cost; // The PBKDF-time-cost in MiB (=> ASN.1-DER-Integer) u32 memory_cost_mib; // The PBKDF-memory-cost (=> ASN.1-DER-Integer) u32 parallelism; // The PBKDF-parallelism-degree (=> ASN.1-DER-Integer) } parameters; // (=> ASN.1-DER-Struct)
These KDFs are currently specified:
- algorithm:
HMAC-SHA2-512
- parameters: None (no further ASN.1 fields in sequence)
These authenticated-encryption-schemes are currently supported:
A ChaCha20-Poly1305 authenticated-encryption-scheme:
- Initialize a ChaCha20-keystream with the given key and a zero-byte-nonce
- Compute 64-keystream bytes (beginning by keystream-byte-offset 0) and use the first 32-bytes as Poly1305-key
- Encrypt the data with the next ChaCha20-keystream-bytes (beginning by keystream-byte-offset 64)
- Compute the Poly1305-MAC (using the key from step 2) over the encrypted data and append it to the encrypted data
Note: In this case, a zero-byte-nonce is not a security-issue because each key has to be unique and thus key-nonce-collisions cannot happen
- algorithm:
ChaCha20+Poly1305@de.KizzyCode.CryptoPipe.v1
- parameters: None (no further ASN.1 fields in sequence)