/blobcrypt

Authenticated encryption for streams and arbitrary large files using libsodium

Primary LanguageCOtherNOASSERTION

Blobcrypt

Blobcrypt is a small library to encrypt and decrypt large files and data streams using libsodium.

Installation

The only prerequisite is libsodium.

Combine the source files by running:

make

And directly drop out/blobcrypt.c and out/blobcrypt.h into your project.

This will also compile example/example, a simple tool to encrypt/decrypt files of any size using an email address and a password.

Usage

Encryption

int blobcrypt_encrypt_init(blobcrypt_encrypt_state *state,
                           int (*write_cb)(void *user_ptr,
                                           unsigned char *buf, size_t len),
                           int (*close_success_cb)(void *user_ptr),
                           int (*close_error_cb)(void *user_ptr),
                           void *user_ptr, unsigned long long total_len,
                           const unsigned char *k);

Initialize the encryption system.

write_cb is a pointer to a function reponsible for writing len bytes from buf into the target file or memory region. It must return 0 on success and -1 on error. Partial writes are not allowed.

close_success_cb is a pointer to a function being called after the encrypted data has been successfully written. If the output is a file, this function might rename a temporary file to its final name.

If the encryption process fails, close_error_cb will be called instead of close_success_cb. If the output is a file, close_error_cb should delete the temporary destination file.

user_ptr is a user-defined pointer passed to write_cb, close_success_cb, and close_error_cb. It can be NULL.

total_len is the total number of bytes to encrypt.

k is a secret key, whose size is blobcrypt_KEYBYTES bytes.

The function returns 0 on success and -1 on error.

int blobcrypt_encrypt_update(blobcrypt_encrypt_state *state,
                             const unsigned char *in,
                             unsigned long long len);

The input data can be provided in any number of segments, of any size. For each call of blobcrypt_encrypt_update, the application must provide a pointer to the first byte of a segment in and the segment length len.

The function returns 0 on success and -1 on error.

int blobcrypt_encrypt_final(blobcrypt_encrypt_state *state);

After total_len bytes have been pushed using blobcrypt_encrypt_update, the function blobcrypt_encrypt_final must be called.

It will finalize the last block and call either close_success_cb or close_error_cb.

The function returns 0 on success and -1 on error.

If any of these functions fail, subsequent calls to encryption functions will return -1 as well, and no more data will be written.

close_error_cb will be called instead of close_success_cb if a previous operation fails, no matter what the function is.

Encrypting a message whose size is dynamic

If the size of a message is dynamic or not known when starting the encryption process, the following function can be used in order to output the header again, with an updated total length:

int blobcrypt_encrypt_truncate(blobcrypt_encrypt_state *state,
                               unsigned long long total_len);

When the output is a file, a typical way to use this function is to pass blobcrypt_UNKNOWNSIZE as the initial size, write the encrypted data, rewind the file and update the header with the actual length:

blobcrypt_encrypt_init();
blobcrypt_encrypt_update();
blobcrypt_encrypt_update();
...
rewind();
blobcrypt_encrypt_truncate();
blobcrypt_encrypt_final();

Decryption

int blobcrypt_decrypt_init(blobcrypt_decrypt_state *state,
                           int (*write_cb)(void *user_ptr,
                                           unsigned char *buf, size_t len),
                           int (*close_success_cb)(void *user_ptr),
                           int (*close_error_cb)(void *user_ptr),
                           void *user_ptr, unsigned long long total_len,
                           const unsigned char *k);

Initialize the decryption system.

write_cb is a pointer to a function reponsible for writing len bytes from buf into the target file or memory region. It must return 0 on success and -1 on error. Partial writes are not allowed.

close_success_cb is a pointer to a function being called after the decrypted data has been successfully written. If the output is a file, this function might rename a temporary file to its final name.

If the decryption process fails, close_error_cb will be called instead of close_success_cb. If the output is a file, close_error_cb should delete the temporary destination file.

user_ptr is a user-defined pointer passed to write_cb, close_success_cb, and close_error_cb. It can be NULL.

total_len is the total number of bytes of the decrypted, if known in advance. This is optional: if the size is not known in advance, total_len should be set to blobcrypt_UNKNOWNSIZE instead.

Providing the expected size allows the decryption process return an error as soon as the first header is decrypted if the size stored in the header doesn't match the expected one.

k is a secret key, whose size is blobcrypt_KEYBYTES bytes.

The function returns 0 on success and -1 on error.

int blobcrypt_decrypt_update(blobcrypt_decrypt_state *state,
                             const unsigned char *in,
                             unsigned long long len);

The input data can be provided in any number of segments, of any size. For each call of blobcrypt_decrypt_update, the application must provide a pointer to the first byte of a segment in and the segment length len.

The function returns 0 on success and -1 on error.

int blobcrypt_decrypt_final(blobcrypt_decrypt_state *state);

After all the input data have been pushed using blobcrypt_decrypt_update, blobcrypt_decrypt_final must be called.

It will check that the expected number of bytes have been decrypted, and call either close_success_cb or close_error_cb.

The function returns 0 on success and -1 on error.

If any of these functions fail, subsequent calls to decryption functions will return -1 as well, and no more data will be written.

close_error_cb will be called instead of close_success_cb if a previous operation fails, no matter what the function is.

Rationale for the API

The API was designed to be as safe as possible even if the application doesn't systematically implement error handling.

In particular, the code flow is always the same no matter what failures happen:

  • *_init()
  • *_update()
  • *_final()

Ideally, the return code of each function call should be tested. If an *_update() operation fails, the encryption/decryption process can be aborted prematurely.

But not doing so will stop calling the write_cb() callback, and safely propagate the failure state to subsequent function calls.

While suboptimal from a performance perspective, testing only the return code of the call to *_final() remains safe.

Predictability: the close_error_cb() callback will only be called by *_final(), and the only callback that can be called by *_update() is write_cb().

Closing a stream or a file descriptor can involve deallocating resources, which can be tricky to do safely if it can happen at any time.

The blobcrypt API ensures that the operation (being close_error_cb() or close_success_cb()) will always happen at the same place.

Finally, the API doesn't perform any memory allocations.

File format

Although the implementation currently only supports sequential read/write, the file format allows random access read, as well as overwriting random blocks. Files can also be truncated and extended, provided that the total length is updated in the header.

Header

Additional data section:

File magic: Bl0Cry\x01\x00
Header length: 4 bytes in little-endian format
Additional data section length: 4 bytes in little-endian format
Nonce: 24 bytes

Encrypted data section:

Message ID: 32 bytes
Block size: 8 bytes in little-endian format (currently: 65536)
Total unencrypted message length: 8 bytes in little-endian format
Authenticator: 16 bytes

Data blocks

Nonce: 24 bytes
Encrypted data: up to <block size> bytes
Authenticator: 16 bytes

The authenticator is calculated using the following additional data, that doesn't have to be stored in data blocks:

Offset of the first byte of the block: 8 bytes in little-endian format
Message ID: 32 bytes, stored in the header

AEAD construction

The header and the data blocks are encrypted and authenticated using an AEAD construction based on the ChaCha20-Poly1305 construction defined by the IETF.

Given an optional 256-bit message ID, a 256-bit secret key k and a 192-bit nonce nonce, a subkey is calculated using the Blake2b hash function with the following parameters:

Personalization: 426c6f6243727970745f4c69622d0100 ("BlobCrypt_Lib-\01\00")
Salt: 128 first bits of the message ID
Key: k
Data: first 96 bits of the nonce
Output size: 256 bits

The encryption and authentication then use the construction described in the ChaCha20-Poly1305 for IETF protocols RFC, with the following parameters:

Key: subkey as described above
Nonce: last 96 bits of the nonce