Installation

The package can be installed by adding decibel to your list of dependencies in mix.exs:

def deps do
  [
    {:decibel, "~> 1.0"}
  ]
end

Documentation can be found at https://hexdocs.pm/decibel.

Getting started

Read the security guidance before choosing a protocol for a real application. These examples keep both peers in one IEx process so they are easy to run. Real peers should follow Decibel for session ownership and message exchange.

Unauthenticated NN

NN establishes an encrypted channel without static-key authentication. The example also supplies a two-byte big-endian length prefix. That prefix is application framing, not part of the Noise message or Decibel's wire format.

protocol = "Noise_NN_25519_ChaChaPoly_BLAKE2s"
initiator = Decibel.new(protocol, :ini)
responder = Decibel.new(protocol, :rsp)

frame = fn noise_message ->
  message = IO.iodata_to_binary(noise_message)
  <<byte_size(message)::unsigned-big-16, message::binary>>
end

unframe = fn <<size::unsigned-big-16, message::binary-size(size)>> -> message end

packet1 = initiator |> Decibel.handshake_encrypt() |> frame.()
"" = Decibel.handshake_decrypt(responder, unframe.(packet1))

packet2 = responder |> Decibel.handshake_encrypt() |> frame.()
"" = Decibel.handshake_decrypt(initiator, unframe.(packet2))
true = Decibel.handshake_complete?(initiator)
true = Decibel.handshake_complete?(responder)

packet3 = initiator |> Decibel.encrypt("hello") |> frame.()
"hello" = Decibel.decrypt(responder, unframe.(packet3))

max_plaintext = :binary.copy(<<0>>, 65_519)
max_message = initiator |> Decibel.encrypt(max_plaintext) |> IO.iodata_to_binary()
65_535 = byte_size(max_message)
max_packet = frame.(max_message)
<<65_535::unsigned-big-16, _::binary-size(65_535)>> = max_packet
^max_plaintext = Decibel.decrypt(responder, unframe.(max_packet))

:ok = Decibel.close(initiator)
:ok = Decibel.close(responder)

See the framing guidance for application responsibilities and Decibel for Decibel's iodata contract.

Authenticated IK

IK lets the initiator authenticate a responder static key it already trusts, and lets the responder authenticate the initiator's static key from the first handshake message.

protocol = "Noise_IK_25519_ChaChaPoly_BLAKE2s"
{initiator_public, _initiator_private} = initiator_static = :crypto.generate_key(:ecdh, :x25519)
{responder_public, _responder_private} = responder_static = :crypto.generate_key(:ecdh, :x25519)

# In a real system these come from certificates, an allow list, pinning, or
# another trust policy independent of this handshake.
trusted_initiator_key = initiator_public
trusted_responder_key = responder_public

initiator =
  Decibel.new(protocol, :ini, %{s: initiator_static, rs: trusted_responder_key})

responder = Decibel.new(protocol, :rsp, %{s: responder_static})

message1 = Decibel.handshake_encrypt(initiator)
"" = Decibel.handshake_decrypt(responder, message1)
^trusted_initiator_key = Decibel.remote_key(responder)

message2 = Decibel.handshake_encrypt(responder)
"" = Decibel.handshake_decrypt(initiator, message2)
^trusted_responder_key = Decibel.remote_key(initiator)

ciphertext = Decibel.encrypt(initiator, "authenticated hello")
"authenticated hello" = Decibel.decrypt(responder, ciphertext)

:ok = Decibel.close(initiator)
:ok = Decibel.close(responder)

Noise authenticates possession of static keys according to the selected pattern; it does not decide whether a key belongs to the intended peer. Compare Decibel.remote_key/1 with an independently trusted value before accepting that peer. See Authentication and key handling.

Usage recipes

Patterns and static keys

Choose a pattern using the pattern and primitive guidance. Generate X25519 static keypairs with :crypto.generate_key(:ecdh, :x25519) and provision them as :s; patterns with a remote static pre-message also require the trusted public key as :rs. Follow the authentication and key-handling guidance for key reuse and trust validation.

Pre-shared keys

After generating a suitable PSK as described in the key-handling guidance, provision the same secret to both peers, select a PSK modifier, and pass PSKs in modifier order:

psk = :crypto.strong_rand_bytes(32)
protocol = "Noise_NNpsk0_25519_ChaChaPoly_BLAKE2s"
initiator = Decibel.new(protocol, :ini, %{psks: [psk]})
responder = Decibel.new(protocol, :rsp, %{psks: [psk]})

Handshake payloads

Pass a payload as the second argument to Decibel.handshake_encrypt/2; its peer receives that payload from Decibel.handshake_decrypt/2. Consult the pattern's payload security properties first: early handshake payloads can be cleartext or have weaker authentication than the completed channel.

Channel binding

After both handshakes complete, Decibel.handshake_hash/1 returns the channel-binding value. Both peers should obtain the same non-nil value; bind it into the application's higher-level authentication protocol where required. See the Noise channel-binding guidance.

Rekeying

Coordinate rekeying by changing the sender's :out key and the recipient's matching :in key:

:ok = Decibel.rekey(sender, :out)
:ok = Decibel.rekey(recipient, :in)

Follow the nonce and rekeying guidance for counter, replay-window, and delayed-message handling.

Fallback and connectionless delivery

For Noise Pipes, including the parser-valid Noise_XXfallback_25519_ChaChaPoly_BLAKE2b protocol, follow the complete fallback example. For unordered or lossy delivery, send {nonce, ciphertext} and use the bounded replay-window example; Decibel exposes nonce selection but the application owns replay rejection.

Ownership, cleanup, and errors

Follow Decibel for session ownership and cleanup, Decibel for the exception contract, and Failure handling for application policy.