Salchicha (Salchicha v0.4.0)

View Source

A pure-ish Elixir cryptography tool for the Salsa20 and ChaCha20 stream ciphers.

This library has a handful of crypto functions that are compatible with NaCl/libsodium for encryption and decryption with shared secret keys. Some sodium-flavored public key cryptography functions are also available, and are primarily wrappers around :crypto functions.

The Salsa20/XSalsa20 ciphers are implemented entirely in Elixir while the Poly1305 MAC function is done through the Erlang :crypto module, which is implemented as a NIF with OpenSSL bindings.

The ChaCha20_Poly1305 AEAD cipher is already supported by the :crypto module, but XChaCha20 is not. The HChaCha20 hash function, an intermediate step for generating an XChaCha20 sub-key, is implemented in Elixir so :crypto.crypto_one_time_aead/7 can be leveraged for XChaCha20_Poly1305.

The ChaCha20/XChaCha20 ciphers do also have pure Elixir implementations just like Salsa20/XSalsa20, but unless you are concerned with long-running NIFs blocking schedulers, you should prefer to use the versions that fully leverage :crypto NIFs, which is the behavior of functions in this module. If you wish to use the elixir implementations, you can call them directly with the functions available in Salchicha.Chacha ending in _pure.

While this module contains everything you'll need to encrypt and decrypt with XSalsa20_Poly1305 and XChaCha20_Poly1305, the internal modules Salchicha.Salsa and Salchicha.Chacha expose a few additional functions including some primitives and non-extended Salsa20 and ChaCha20 ciphers.

Examples

Assume we have a key, and an extended nonce, and a plaintext message

key = Salchicha.generate_secret_key()
nonce = Salchicha.generate_nonce()
message = "Hello, World!"

XSalsa20 Poly1305 via secretbox/3 and secretbox_open/3

encrypted_message =
  message
  |> Salchicha.secretbox(nonce, key)
  |> IO.iodata_to_binary()
# <<211, 79, 12, ...>>

decrypted_message =
  encrypted_message
  |> Salchicha.secretbox_open(nonce, key)
  |> IO.iodata_to_binary()
# "Hello, World!"

The secretbox'd message is in the format | --- 16-byte tag --- | --- cipher text --- |

Note the IO.iodata_to_binary/1 calls are optional. The input messages can be iodata/0.

XChaCha20 Poly1305 in combined mode

encrypted_message =
  message
  |> Salchicha.xchacha20_poly1305_encrypt(nonce, key, _aad = "XCHACHA")
  |> IO.iodata_to_binary()
# <<82, 26, 161, ...>>

decrypted_message =
  encrypted_message
  |> Salchicha.xchacha20_poly1305_decrypt(nonce, key, _aad = "XCHACHA")
# "Hello, World!"

The combined mode encrypted message is in the format | --- cipher text --- | --- 16-byte tag --- |

The AAD is optional and will default to <<>>, a zero-length binary.

XChaCha20 Poly1305 in detached mode

{cipher_text, tag} =
  message
  |> Salchicha.xchacha20_poly1305_encrypt_detached(nonce, key, _aad = "XCHACHA")
# {<<82, 26, 161, ...>>, <<1, 199, 251, ...>>}

decrypted_message =
  cipher_text
  |> Salchicha.xchacha20_poly1305_decrypt_detached(nonce, key, _aad = "XCHACHA", tag)
# "Hello, World!"

Detached mode means the cipher text and tag are returned separately instead of being concatenated together.

Curve25519 XSalsa20 Poly1305 via box/4 and box_open/4

{alice_public, alice_private} = Salchicha.generate_box_keypair()
{bob_public, bob_private} = Salchicha.generate_box_keypair()

{shared_secret, encrypted_message} = Salchicha.box(message, nonce, bob_public, alice_private)
# {<<192, 50, 31, ...>>, [<<89, 217, 187, ...>>, ...]}

{^shared_secret, decrypted_message_by_keypair} =
  Salchicha.box_open(encrypted_message, nonce, alice_public, bob_private)

IO.iodata_to_binary(decrypted_message_by_keypair)
# "Hello, World!"

decrypted_message_by_shared_secret =
  encrypted_message
  |> Salchicha.secretbox_open(nonce, shared_secret)
  |> IO.iodata_to_binary()
# "Hello, World!"

The shared_secret output key can be used with secretbox/3 and secretbox_open/3 for subsequent messages to avoid computing the shared secret from the public and private keys each time.

Ed25519 Message Signatures

{public_key, private_key} = Salchicha.generate_sign_keypair()

signature = Salchicha.sign(message, private_key)
# <<51, 62, 180, ...>>

Salchicha.signature_valid?(message, signature, public_key)
# true

Salchicha.signature_valid?(message <> "~~~", signature, public_key)
# false

Summary

Types

Additional authenticated data

12-byte nonce used by the ChaCha20 (IETF) cipher

Encrypted plaintext

Encrypted message to decrypt; cipher_text/0 appended or prepended with tag/0

24-byte extended nonce used by the XSalsa20 and XChaCha20 ciphers

Plaintext message to encrypt

32-byte private key of a Curve25519 (X25519 or Ed25519) key pair

32-byte public key of a Curve25519 (X25519 or Ed25519) key pair

8-byte nonce used by the Salsa20 cipher

32-byte shared secret key used by all variations of Salsa/ChaCha

64-byte Ed25519 message signature

Tag or MAC (message authentication code)

Functions

Encrypts a message with your private key and the recipient's public key using Curve25519_XSalsa20_Poly1305.

Decrypts a message with your private key and the sender's public key using Curve25519_XSalsa20_Poly1305.

Computes the shared secret key given a peer's X25519 public key and your X25519 private key.

Generates a random X25519 key pair for public-key encryption (box/4 and box_open/4).

Generates a random 24-byte extended nonce

Generates a random 32-byte key

Generates a random Ed25519 key pair for message signing and signature verification.

Encrypts a message with a secret key using the XSalsa20_Poly1305 authenticated cipher.

Decrypts a message that was encrypted with secretbox/3 using the XSalsa20_Poly1305 authenticated cipher.

Sign a message with an Ed25519 private key

Verifies a message signature with the signer's Ed25519 public key

Decrypts a message that was encrypted in "combined mode" using the XChaCha20_Poly1305 AEAD cipher.

Decrypts a message that was encrypted in "detacheded mode" using the XChaCha20_Poly1305 AEAD cipher.

Encrypts a message with a secret key using the XChaCha20_Poly1305 AEAD cipher in "combined mode".

Encrypts a message with a secret key using the XChaCha20_Poly1305 AEAD cipher in "detached mode".

Types

aad()

@type aad() :: iodata()

Additional authenticated data

chacha_nonce()

(since 0.2.0)
@type chacha_nonce() :: <<_::96>>

12-byte nonce used by the ChaCha20 (IETF) cipher

cipher_text()

@type cipher_text() :: binary()

Encrypted plaintext

encrypted_message()

@type encrypted_message() :: iodata()

Encrypted message to decrypt; cipher_text/0 appended or prepended with tag/0

extended_nonce()

@type extended_nonce() :: <<_::192>>

24-byte extended nonce used by the XSalsa20 and XChaCha20 ciphers

message()

@type message() :: iodata()

Plaintext message to encrypt

private_key()

(since 0.4.0)
@type private_key() :: <<_::256>>

32-byte private key of a Curve25519 (X25519 or Ed25519) key pair

public_key()

(since 0.4.0)
@type public_key() :: <<_::256>>

32-byte public key of a Curve25519 (X25519 or Ed25519) key pair

salsa_nonce()

(since 0.2.0)
@type salsa_nonce() :: <<_::64>>

8-byte nonce used by the Salsa20 cipher

secret_key()

@type secret_key() :: <<_::256>>

32-byte shared secret key used by all variations of Salsa/ChaCha

signature()

(since 0.4.0)
@type signature() :: <<_::512>>

64-byte Ed25519 message signature

tag()

@type tag() :: <<_::128>>

Tag or MAC (message authentication code)

Functions

box(message, nonce, their_public, your_private)

(since 0.4.0)
@spec box(message(), extended_nonce(), public_key(), private_key()) ::
  {secret_key(), iolist()}

Encrypts a message with your private key and the recipient's public key using Curve25519_XSalsa20_Poly1305.

This function behaves like crypto_box() and crypto_box_easy() do in NaCl and libsodium.

Parameters

  • message - Plaintext message to be encrypted
  • nonce - 24-byte extended nonce
  • their_public - Recipient's 32-byte public X25519 key
  • your_private - Your 32-byte private X25519 key

Returns a tuple with the computed shared secret key and the encrypted message.

The returned shared secret key can be used with secretbox/3 for subsequent encrypted messages to the same recipient to avoid having to re-compute the key with box/4 for each message. Doing this would be similar to using crypto_box_afternm() in NaCl, which uses the pre-computed shared secret for multiple messages.

Calls compute_shared_secret/2 then calls secretbox/3 with the result.

box_open(message, nonce, their_public, your_private)

(since 0.4.0)
@spec box_open(encrypted_message(), extended_nonce(), public_key(), private_key()) ::
  {secret_key(), iolist()} | :error

Decrypts a message with your private key and the sender's public key using Curve25519_XSalsa20_Poly1305.

This function behaves like crypto_box_open() and crypto_box_open_easy() do in NaCl and libsodium.

Parameters

  • message - The encrypted message
  • nonce - 24-byte extended nonce
  • their_public - Sender's 32-byte public X25519 key
  • your_private - Your 32-byte private X25519 key

Returns a tuple with the computed shared secret key and the decrypted plaintext.

The returned shared secret key can be used with secretbox_open/3 for subsequent messages from the same sender to avoid having to re-compute the key with box_open/4 for each message. Doing this would be similar to using crypto_box_open_afternm() in NaCl, which uses the pre-computed shared secret for multiple messages.

Calls compute_shared_secret/2 then calls secretbox_open/3 with the result.

compute_shared_secret(their_public, your_private)

(since 0.4.0)
@spec compute_shared_secret(public_key(), private_key()) :: secret_key()

Computes the shared secret key given a peer's X25519 public key and your X25519 private key.

Similar to NaCl/libsodium crypto_box_beforenm()/crypto_box_open_beforenm().

Calls :crypto.compute_key/4 then calls Salchicha.Salsa.hsalsa20/2 with the resultant key and 16 zeroes as the input vector.

generate_box_keypair(private_key \\ nil)

(since 0.4.0)
@spec generate_box_keypair(private_key() | nil) :: {public_key(), private_key()}

Generates a random X25519 key pair for public-key encryption (box/4 and box_open/4).

If a private key is provided it will recover the public key instead of generating a random key pair.

Behaves like NaCl/libsodium crypto_box_keypair(). Calls :crypto.generate_key/3

generate_nonce()

@spec generate_nonce() :: extended_nonce()

Generates a random 24-byte extended nonce

XSalsa20 and XChaCha20 use a 24-byte nonce, up from the 8 and 8/12 byte nonces of the respective Salsa20 and ChaCha20 ciphers.

You should never reuse the same nonce for a given secret key. 24 bytes are said to be large enough to generate nonces randomly - doing so would be ill-advised with 8-byte nonces since collision would be much more likely.

generate_secret_key()

@spec generate_secret_key() :: secret_key()

Generates a random 32-byte key

generate_sign_keypair(private_key \\ nil)

(since 0.4.0)
@spec generate_sign_keypair(private_key() | nil) :: {public_key(), private_key()}

Generates a random Ed25519 key pair for message signing and signature verification.

If a private key is provided it will recover the public key instead of generating a random key pair.

Behaves like NaCl/libsodium crypto_sign_keypair(). Calls :crypto.generate_key/3

secretbox(message, nonce, key)

@spec secretbox(message(), extended_nonce(), secret_key()) :: iolist()

Encrypts a message with a secret key using the XSalsa20_Poly1305 authenticated cipher.

This function behaves like crypto_secretbox() does in NaCl.

Parameters

  • message - Plaintext message to be encrypted
  • nonce - 24-byte extended nonce
  • key - 32-byte secret key

The return value is the cipher text prepended by the 16-byte tag (MAC), compatible with NaCl.

Returns an iolist/0 to reduce binary copies. Call IO.iodata_to_binary/1 if you need a single binary.

Calls Salchicha.Salsa.xsalsa20_poly1305_encrypt/3 then concatenates the tag and cipher text

secretbox_open(message, nonce, key)

@spec secretbox_open(encrypted_message(), extended_nonce(), secret_key()) ::
  iolist() | :error

Decrypts a message that was encrypted with secretbox/3 using the XSalsa20_Poly1305 authenticated cipher.

This function behaves like crypto_secretbox_open() does in NaCl.

Parameters

  • message - The encrypted message (tag prepended to cipher text)
  • nonce - 24-byte extended nonce
  • key - 32-byte secret key

The return value is the decrypted plaintext (as an iolist) or :error if authentication failed.

Returns an iolist/0 to reduce binary copies. Call IO.iodata_to_binary/1 if you need the message as a binary.

Splits tag and cipher text then calls Salchicha.Salsa.xsalsa20_poly1305_decrypt/4

sign(message, private_key)

(since 0.4.0)
@spec sign(message(), private_key()) :: signature()

Sign a message with an Ed25519 private key

Behaves like NaCl/libsodium crypto_sign(). Calls :crypto.sign/4

signature_valid?(message, signature, public_key)

(since 0.4.0)

Verifies a message signature with the signer's Ed25519 public key

Behaves like NaCl/libsodium crypto_sign_open(). Calls :crypto.sign/4

xchacha20_poly1305_decrypt(message, nonce, key, aad \\ <<>>)

@spec xchacha20_poly1305_decrypt(
  encrypted_message(),
  extended_nonce(),
  secret_key(),
  aad()
) ::
  binary() | :error

Decrypts a message that was encrypted in "combined mode" using the XChaCha20_Poly1305 AEAD cipher.

This function behaves like crypto_aead_xchacha20poly1305_ietf_decrypt() does in libsodium.

Parameters

  • message - The encrypted message (tag appended to cipher text)
  • nonce - 24-byte extended nonce
  • key - 32-byte secret key
  • aad - Additional authenticated data (defaults to <<>> i.e. no AAD)

The return value is the decrypted plaintext as a binary or :error if authentication failed.

Splits cipher text and tag then calls Salchicha.Chacha.xchacha20_poly1305_decrypt/5

xchacha20_poly1305_decrypt_detached(cipher_text, nonce, key, aad \\ <<>>, tag)

@spec xchacha20_poly1305_decrypt_detached(
  encrypted_message(),
  extended_nonce(),
  secret_key(),
  aad(),
  tag()
) :: binary() | :error

Decrypts a message that was encrypted in "detacheded mode" using the XChaCha20_Poly1305 AEAD cipher.

This function behaves like crypto_aead_xchacha20poly1305_ietf_decrypt_detached() does in libsodium.

Parameters

  • cipher_text - The encrypted message (only the cipher text, not appended with the tag)
  • nonce - 24-byte extended nonce
  • key - 32-byte secret key
  • aad - Additional authenticated data (defaults to <<>> i.e. no AAD)
  • tag - 16-byte Poly1305 authentication tag or MAC

The return value is the decrypted plaintext as a binary or :error if authentication failed.

This function differs from xchacha20_poly1305_decrypt/4 by returning the tag and cipher text separately

This "detached mode" function differs from the "combined mode" xchacha20_poly1305_decrypt/4 in that the cipher text and tag are supplied as separate parameters, not combined as a single message.

Calls Salchicha.Chacha.xchacha20_poly1305_decrypt/5

xchacha20_poly1305_encrypt(message, nonce, key, aad \\ <<>>)

@spec xchacha20_poly1305_encrypt(message(), extended_nonce(), secret_key(), aad()) ::
  iolist()

Encrypts a message with a secret key using the XChaCha20_Poly1305 AEAD cipher in "combined mode".

This function behaves like crypto_aead_xchacha20poly1305_ietf_encrypt() does in libsodium.

Parameters

  • message - Plaintext message to be encrypted
  • nonce - 24-byte extended nonce
  • key - 32-byte secret key
  • aad - Additional authenticated data (defaults to <<>> i.e. no AAD)

The return value is the cipher text appended by the 16-byte tag (MAC), i.e. "combined mode".

Returns an iolist/0 to reduce binary copies. Call IO.iodata_to_binary/1 if you need a single binary.

Calls Salchicha.Chacha.xchacha20_poly1305_encrypt/4 then concatenates the cipher text and tag

xchacha20_poly1305_encrypt_detached(message, nonce, key, aad \\ <<>>)

@spec xchacha20_poly1305_encrypt_detached(
  message(),
  extended_nonce(),
  secret_key(),
  aad()
) ::
  {cipher_text(), tag()}

Encrypts a message with a secret key using the XChaCha20_Poly1305 AEAD cipher in "detached mode".

This function behaves like crypto_aead_xchacha20poly1305_ietf_encrypt_detached() does in libsodium.

Parameters

This "detached mode" function differs from the "combined mode" xchacha20_poly1305_encrypt/4 by returning the tag and cipher text separately in a tuple in the form {cipher_text, tag}. Both cipher_text and tag will already be binaries.

Calls Salchicha.Chacha.xchacha20_poly1305_encrypt/4