Salchicha (Salchicha v0.4.0)
View SourceA 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
@type aad() :: iodata()
Additional authenticated data
@type chacha_nonce() :: <<_::96>>
12-byte nonce used by the ChaCha20 (IETF) cipher
@type cipher_text() :: binary()
Encrypted plaintext
@type encrypted_message() :: iodata()
Encrypted message to decrypt; cipher_text/0
appended or prepended with tag/0
@type extended_nonce() :: <<_::192>>
24-byte extended nonce used by the XSalsa20 and XChaCha20 ciphers
@type message() :: iodata()
Plaintext message to encrypt
@type private_key() :: <<_::256>>
32-byte private key of a Curve25519 (X25519 or Ed25519) key pair
@type public_key() :: <<_::256>>
32-byte public key of a Curve25519 (X25519 or Ed25519) key pair
@type salsa_nonce() :: <<_::64>>
8-byte nonce used by the Salsa20 cipher
@type secret_key() :: <<_::256>>
32-byte shared secret key used by all variations of Salsa/ChaCha
@type signature() :: <<_::512>>
64-byte Ed25519 message signature
@type tag() :: <<_::128>>
Tag or MAC (message authentication code)
Functions
@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 encryptednonce
- 24-byte extended noncetheir_public
- Recipient's 32-byte public X25519 keyyour_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.
@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 messagenonce
- 24-byte extended noncetheir_public
- Sender's 32-byte public X25519 keyyour_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.
@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
@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.
@spec generate_secret_key() :: secret_key()
Generates a random 32-byte key
@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
@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 encryptednonce
- 24-byte extended noncekey
- 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
@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 noncekey
- 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
@spec sign(message(), private_key()) :: signature()
Sign a message with an Ed25519 private key
Behaves like NaCl/libsodium crypto_sign()
. Calls :crypto.sign/4
Verifies a message signature with the signer's Ed25519 public key
Behaves like NaCl/libsodium crypto_sign_open()
. Calls :crypto.sign/4
@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 noncekey
- 32-byte secret keyaad
- 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
@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 noncekey
- 32-byte secret keyaad
- 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.
@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 encryptednonce
- 24-byte extended noncekey
- 32-byte secret keyaad
- 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
@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.