Salchicha.Chacha (Salchicha v0.4.0)

View Source

Implementation of the ChaCha20 and XChaCha20 Ciphers

Internal module

This module is documented for completeness and for curious cryptographers. It contains some ChaCha primitives that you likely won't need to use directly.

Purpose

Erlang's :crypto module supports the chacha20_poly1305 AEAD stream cipher, standardized by the IETF, as its underlying NIFs are bindings to OpenSSL, which implements it.

Analogously to Salsa20 and XSalsa20, XChaCha20 is a way to use 192-bit nonces with ChaCha20 by hashing the key and part of the extended nonce to generate a sub-key, which is used as the input key for ChaCha20.

To leverage the crypto module, we had to implement the HChaCha20 hash function in elixir to then pass the resulting sub-key and sub-nonce to :crypto.crypto_one_time_aead/7.

Implementation

The HChaCha20 function takes the first 16 bytes of the extended 24-byte XChaCha20 nonce, expands the key and the 16-byte nonce slice into a block in place of the block counter and usual smaller nonce. That block has 20 rounds of mutation, and instead of summing the block with its starting state as is done with keystream generation, 8 of the 16 32-bit words are taken and used as the sub-key, which is the input key for the chacha20 cipher. The sub-nonce is the latter 8 bytes of the extended nonce prepended with 4 zero bytes for the 12-byte nonce that the IETF version of ChaCha20 specifies.

After running HChaCha20, we have the inputs required use the :crypto module's :chacha20_poly1305 functionality in the capacity of XChaCha20_Poly1305. This is all in service of leveraging the performance benefits of the crypto NIFs, which are necessarily going to be more performant than anything implemented in pure elixir/erlang like the :kcl package.

For reference, I also implemented the ChaCha20/XChaCha20 functions in Elixir that don't use crypto_one_time_aead/7, only leveraging :crypto.mac/3 for the Poly1305 MAC similarly to Salchicha.Salsa. The only reason you might prefer to use these functions (ending in _pure) over the NIF ones is that if you have an exceptionally large message the long-running NIF would not yield to the erlang scheduler and could block other processes. In most cases you should prefer the impure variants - the Salchicha functions will use them by default.

ChaCha20 is a variant of the Salsa20 cipher. I will discuss in greater detail the implementation in the Salchicha.Salsa module, where much is applicable here.

References for Salsa family of ciphers

Performance considerations

After the XChaCha20 sub-key is generated in elixir, the crypto NIF function performs the heavy lifting. Performance should be speedy.

Summary

Functions

chacha20_poly1305_decrypt_pure(cipher_text, nonce, key, aad, tag)

(since 0.2.0)
@spec chacha20_poly1305_decrypt_pure(
  cipher_text :: iodata(),
  Salchicha.chacha_nonce(),
  Salchicha.secret_key(),
  Salchicha.aad(),
  Salchicha.tag()
) :: plain_text :: iolist() | :error

chacha20_poly1305_encrypt_pure(plain_text, nonce, key, aad)

(since 0.2.0)
@spec chacha20_poly1305_encrypt_pure(
  Salchicha.message(),
  Salchicha.chacha_nonce(),
  Salchicha.secret_key(),
  Salchicha.aad()
) :: {cipher_text :: iolist(), Salchicha.tag()}

chacha20_xor(message, nonce, key, initial_block_counter \\ 0)

(since 0.2.0)
@spec chacha20_xor(
  message :: iodata(),
  Salchicha.chacha_nonce(),
  Salchicha.secret_key(),
  non_neg_integer()
) :: iolist()

Plain ChaCha20 Cipher. Crypto primitive.

XOR a message (encrypt or decrypt) with ChaCha20. This uses 12 byte nonces and isn't authenticated with Poly1305 MAC.

The IETF standardized version of ChaCha20 uses 12 byte nonces and a 4 byte block counter instead of 8 byte nonces and an 8 byte block counter. The IETF standard's 4 byte (32 bit) block counter means messages are limited to 256GB: 2^32 blocks (4 GigaBlocks) * 64 bytes per block = 256GB.

Because of how the block state is arranged in the cipher, if you take an 8-byte nonce and prepend it with 4 zeros to form a 12 byte nonce, this is equivalent to the pre-IETF-standardized version of ChaCha20 that uses 8 byte nonces. In fact, the XChaCha20 sub-nonce derived from the 24-byte extended nonce is simply the last 8 byte prepended with 4 zeros.

Block counter starts at 0 by default.

Implemented in Elixir.

Equivalent NIF-powered version of this would be

# Encrypt
:crypto.crypto_one_time(:chacha20, key, iv, plaintext, _encrypt = true)

# Decrypt
:crypto.crypto_one_time(:chacha20, key, iv, ciphertext, _encrypt = false)

Some things worth noting from the above usage of the :crypto module function:

  • The initialization vector (iv) arg is NOT simply the nonce - it the full 16-byte user-controlled input.
    • For an 8 or 12 byte nonce, you'd have to prepend your initial block counter in little-endian.
      • iv = <<_block_counter=0::little-32>> <> my_twelve_byte_nonce
      • iv = <<_block_counter=0::little-64>> <> my_eight_byte_nonce
    • How annoying!
  • The fifth argument, the boolean encrypt flag, is irrelevant.
    • This is an unauthenticated stream cipher where the input message is XOR'd with the keystream.
      • Encryption and decryption are fundamentally the same operation.
      • While the flag is required, whether it's true or false makes no difference.
    • How annoying!

hchacha20(key, nonce)

@spec hchacha20(Salchicha.secret_key(), Salchicha.extended_nonce()) ::
  Salchicha.secret_key()
@spec hchacha20(Salchicha.secret_key(), input_vector :: <<_::128>>) ::
  Salchicha.secret_key()

HChaCha20 hash function for deriving a sub-key for XChaCha20. Crypto primitive.

Second arg can be extended nonce (first 16 bytes of the nonce will be used) or 16 bytes.

xchacha20_poly1305_decrypt(cipher_text, nonce, key, aad, tag)

@spec xchacha20_poly1305_decrypt(
  cipher_text :: iodata(),
  Salchicha.extended_nonce(),
  Salchicha.secret_key(),
  Salchicha.aad(),
  Salchicha.tag()
) :: plain_text :: binary() | :error

xchacha20_poly1305_decrypt_pure(plain_text, nonce, key, aad, tag)

(since 0.2.0)
@spec xchacha20_poly1305_decrypt_pure(
  cipher_text :: iodata(),
  Salchicha.extended_nonce(),
  Salchicha.secret_key(),
  Salchicha.aad(),
  Salchicha.tag()
) :: plain_text :: iolist() | :error

xchacha20_poly1305_encrypt(plain_text, nonce, key, aad)

@spec xchacha20_poly1305_encrypt(
  Salchicha.message(),
  Salchicha.extended_nonce(),
  Salchicha.secret_key(),
  Salchicha.aad()
) :: {cipher_text :: binary(), Salchicha.tag()}

xchacha20_poly1305_encrypt_pure(plain_text, nonce, key, aad)

(since 0.2.0)
@spec xchacha20_poly1305_encrypt_pure(
  Salchicha.message(),
  Salchicha.extended_nonce(),
  Salchicha.secret_key(),
  Salchicha.aad()
) :: {cipher_text :: iolist(), Salchicha.tag()}