Decibel is an implementation of The Noise Protocol Framework.
Noise is a framework for building crypto protocols. Noise protocols support mutual and optional authentication, identity hiding, forward secrecy, zero round-trip encryption, and other advanced features.
For more information about Noise, its rationale, supported protocols etc, please refer to The Noise Specification.
The rest of this document assumes the reader is familiar with the above specification.
API conventions
Decibel has a direct, raising API. Operations that produce data return it
directly: a session handle, ciphertext or plaintext, a boolean, a handshake
hash, a nonce, or a remote key. Operations whose only result is a state change
return :ok: close/1, rekey/2, and set_nonce/3.
Plaintext, associated data, and inbound Noise messages accept iodata. The
message and payload return types are also iodata, so callers that need a
binary for framing or transport I/O should use IO.iodata_to_binary/1 rather
than depend on an incidental list or binary shape.
There are no bang and non-bang variants. Invalid construction and arguments
raise ArgumentError; peer-message failures raise
Decibel.DecryptionError; nonce and one-way direction failures raise
Decibel.NonceError and Decibel.TransportDirectionError; and ownership,
lifetime, or phase failures raise Decibel.SessionError. Applications should
rescue these stable exceptions only at boundaries where they have an explicit
recovery or failure policy. Rejected operations do not commit session state.
Overview
Decibel encrypts and decrypts messages according to the Noise Protocol, and the client's selection of handshake and cryptographic primitives. It does not act as a transport, nor does it say anything about how Noise messages should be transmitted between participants.
Each party - either the initiator (the party that starts the handshake) or
responder (the other party) - advances the handshake until it completes, at
which point a secure channel is established. Interactive handshakes establish
bidirectional transport. The one-way N, K, and X patterns permit only the
initiator to encrypt and only the responder to decrypt transport messages.
Decibel supports all the handshake patterns outlined in r34 of the specification including the fundamental patterns, deferred patterns and one-way patterns. It also supports pre-shared keys as outlined in the specification, and fallback handshakes for Noise Pipes support.
Example
Consider the unauthenticated NN handshake defined in the Noise Protocol:
NN:
-> e
<- e, eeThe parties agree on this handshake and its cryptographic parameters and express this in a protocol name. This runnable example drives both roles in one process; a real peer process must create and operate its own session and exchange framed Noise messages rather than handles.
iex> initiator = Decibel.new("Noise_NN_25519_AESGCM_SHA256", :ini)
iex> responder = Decibel.new("Noise_NN_25519_AESGCM_SHA256", :rsp)
iex> Decibel.handshake_encrypt(initiator) |> then(&Decibel.handshake_decrypt(responder, &1))
""
iex> Decibel.handshake_encrypt(responder) |> then(&Decibel.handshake_decrypt(initiator, &1))
""
iex> ciphertext = Decibel.encrypt(initiator, "Hello, world")
iex> Decibel.decrypt(responder, ciphertext)
"Hello, world"
iex> {Decibel.close(initiator), Decibel.close(responder)}
{:ok, :ok}The Getting Started guide adds authenticated key validation, application framing, the exact message-size boundary, and focused usage recipes.
Lifecycle
Ownership and lifetime
new/4 returns an opaque session/0 handle. The session state belongs to
the process that calls new/4 and is stored in that process until close/1
is called or the owner process exits. A handle contains no cryptographic state
and must not be inspected, altered, or constructed by callers.
Every operation on a session must run serially in its owner process. Do not
pass the handle to a task, worker, or peer process, and do not call it
concurrently. Pass Noise messages and application data between processes
instead. A GenServer or similar long-lived process can own a session and
serialize all operations in its callbacks. If that process terminates, its
supervisor must establish a new session; the old one cannot be recovered or
transferred.
Decibel does not support session ownership transfer. Using a structurally
valid handle in another process raises Decibel.SessionError with
reason: :not_owner, including when the owner has closed it or exited. This
classification uses the handle's owner PID alone by design; Decibel has no
handle registry, issuance proof, or signature. A legacy bare reference,
malformed value, or unknown owner-local handle uses reason: :unknown. After
close/1, every operation by the owner, including another close, uses
reason: :closed.
During a live handshake, only the operation for the next pattern message is
permitted. handshake_encrypt/2 requires the :handshake_write phase and
handshake_decrypt/2 requires :handshake_read. Transport and cipher-state
operations require :transport. A phase-invalid call raises
Decibel.SessionError with reason: :wrong_phase, the operation, and the
expected and actual phases, without changing session state.
Creation
Each party begins by creating a new handshake, via new/4, specifying the
protocol name,
the role the party plays in the handshake (:ini for initiator, :rsp for
responder), and optionally any pre-message keys.
# In the IK handshake, the responder's public (static) key is known to the
# initiator prior to the handshake.
keys = %{s: {<<...>>, <<...>>}, rs: <<...>>}
ini = Decibel.new("Noise_IK_448_ChaChaPoly_BLAKE2b", :ini, keys)The result of new/4 is an opaque owner-aware handle used for the rest of the
session. It is valid only in the process that created it.
Handshake
During the handshake phase, the protocol is advanced by each party in turn. For
initiators, this typically starts with calling handshake_encrypt/2 and sending
the result to the responder. In turn, the responder calls handshake_decrypt/2
before typically encrypting its own handshake message and sending that to the
initiator.
This sequence continues until the handshake is complete. If the selected protocol
is known at compile time, the parties can just assume its completion in the
absence of an error (as in the example above). Alternatively,
each party can call handshake_complete?/1 after each handshake
encryption/decryption.
Once the handshake is complete, a secure channel is established with the properties of the selected protocol.
Additionally, once the handshake is complete, a unique 'session-hash' is available
via handshake_hash/1 - see the channel-binding
section of the specification for more details.
Session
Once the handshake is complete, the parties use encrypt/3 and decrypt/3 to
exchange 'application' messages between each other. Both functions provide for optional
'associated authenticated data' to be specified, that provides message-integrity
assurance for the application data.
Interactive handshake patterns allow both parties to encrypt and decrypt
transport messages. For the one-way N, K, and X patterns, only the
initiator may encrypt and only the responder may decrypt. Reverse-direction
transport and cipher-management operations raise
Decibel.TransportDirectionError without changing session state.
Each call encrypts or decrypts exactly one Noise transport message. Noise messages are limited to 65,535 bytes, so transport plaintexts are limited to 65,519 bytes after allowing for the 16-byte authentication tag. Applications must split and frame larger logical messages before passing them to Decibel.
Each keyed channel may use nonces from 0 through 2^64 - 2. Consuming the
final nonce exhausts that channel; later encryption or decryption raises
Decibel.NonceError. An exhausted channel cannot be revived by rekey/2,
so the application must close it and establish a new session.
When the application is finished with the session, each party should call
close/1 to discard its cryptographic state. Owner-process termination also
discards the state automatically.
Summary
Types
A Noise handshake hash.
Key material and prologue data used to initialize a handshake.
A public-private Diffie-Hellman keypair.
A cipher nonce, including Noise's reserved exhausted value.
The role whose outbound channel uses the first split key.
The role the party plays in the protocol.
An opaque, process-owned Noise session handle.
A nonce value that may be selected for an active cipher.
Functions
Release the resources associated with the session.
Decrypts a message over an established session, using an optionally provided AAD for message integrity.
Encrypts a message over an established session, using an optionally provided AAD for message integrity.
Deprecated alias for handshake_hash/1.
Deprecated alias for nonce/2.
Deprecated alias for remote_key/1.
Returns true if the handshake is complete, false otherwise.
Decrypt an inbound handshake message, returning any optionally provided application data.
Encrypt an outbound handshake message, optionally folding in application data.
Returns the handshake hash unique to the established session.
Deprecated alias for handshake_complete?/1.
Start a new handshake.
Get the current nonce value of the specified cipher.
Rekey the inbound or outbound channel of the session.
Get the remote (static) key if available.
Set the current value of nonce for the specified cipher.
Types
@type handshake_hash() :: <<_::256>> | <<_::512>>
A Noise handshake hash.
SHA256 and BLAKE2s produce 32-byte hashes; SHA512 and BLAKE2b
produce 64-byte hashes.
@type key_material() :: %{ optional(:s) => keypair(), optional(:rs) => binary(), optional(:e) => keypair(), optional(:re) => binary(), optional(:psks) => [<<_::256>>], optional(:prologue) => iodata() }
Key material and prologue data used to initialize a handshake.
A public-private Diffie-Hellman keypair.
@type nonce() :: 0..18_446_744_073_709_551_615
A cipher nonce, including Noise's reserved exhausted value.
@type option() :: {:swap, role()}
The role whose outbound channel uses the first split key.
@type role() :: :ini | :rsp
The role the party plays in the protocol.
@type session() :: Decibel.Session.t()
An opaque, process-owned Noise session handle.
@type usable_nonce() :: 0..18_446_744_073_709_551_614
A nonce value that may be selected for an active cipher.
Functions
@spec close(session()) :: :ok
Release the resources associated with the session.
Returns :ok after discarding the session state.
This discards handshake or transport state immediately, including pending key material. The state is also released automatically when the owner process terminates. The handle remains closed and cannot be reused.
Invalid ownership or an unknown handle raises Decibel.SessionError. Calling
close/1 again raises it with reason: :closed.
Decrypts a message over an established session, using an optionally provided AAD for message integrity.
Returns the decrypted message. Raises Decibel.DecryptionError with
reason: :truncated or :authentication_failed if the message cannot be
decrypted. The inbound state and nonce remain unchanged on either failure.
Raises ArgumentError if the message exceeds 65,535 bytes.
Raises Decibel.TransportDirectionError before any state change if inbound
transport is not permitted by a one-way handshake.
Raises Decibel.NonceError without changing state if the inbound channel's
nonce is exhausted.
See Failure handling for the policy an application must apply to unauthenticated transport messages.
Requires the :transport phase. Invalid ownership, a closed/unknown handle,
or use during the handshake raises Decibel.SessionError before any state
change.
Encrypts a message over an established session, using an optionally provided AAD for message integrity.
Returns the encrypted message.
The application must provide framing and authenticated termination and follow the nonce and rekeying guidance.
Raises ArgumentError if plaintext exceeds 65,519 bytes, the largest plaintext
that leaves room for the 16-byte authentication tag within a Noise message.
Raises Decibel.TransportDirectionError before any state change if outbound
transport is not permitted by a one-way handshake.
Raises Decibel.NonceError without changing state if the outbound channel's
nonce is exhausted.
Requires the :transport phase. Invalid ownership, a closed/unknown handle,
or use during the handshake raises Decibel.SessionError before any state
change.
@spec get_handshake_hash(session()) :: handshake_hash() | nil
Deprecated alias for handshake_hash/1.
Scheduled for removal in Decibel 2.0.
Deprecated alias for nonce/2.
Scheduled for removal in Decibel 2.0.
Deprecated alias for remote_key/1.
Scheduled for removal in Decibel 2.0.
Returns true if the handshake is complete, false otherwise.
This accessor is valid during either handshake turn and transport. Invalid
ownership or a closed/unknown handle raises Decibel.SessionError.
Decrypt an inbound handshake message, returning any optionally provided application data.
Raises Decibel.DecryptionError with reason: :truncated,
:authentication_failed, or :invalid_public_key if the peer message cannot be
processed. Its :remote_keys field contains keys processed before the failure,
and the stored handshake state remains unchanged. Raises ArgumentError if the
message exceeds 65,535 bytes.
See Failure handling before deciding whether to abandon the handshake or enter a reviewed fallback protocol.
Requires the session's :handshake_read phase. Ownership, closed/unknown
handles, and a wrong handshake turn raise Decibel.SessionError before any
state change.
Encrypt an outbound handshake message, optionally folding in application data.
The reader is encouraged to understand the ramifications of providing application data during the handshake. As the handshake is not yet completed, the properties of any secure channel have not yet been established. Such data may even be sent in the clear. Consult the Payload Security Properties in the specification for more information.
Applications are responsible for framing and authenticating termination and for their failure policy.
Raises ArgumentError if the complete handshake message would exceed the Noise
limit of 65,535 bytes. The maximum application-data size varies with the handshake
pattern and cryptographic primitives because public keys and authentication tags
are part of the same message.
A peer public key received in an earlier message might not be used until this
write step. If that key is invalid, this function raises
Decibel.DecryptionError with reason: :invalid_public_key. The session state
remains unchanged so the caller can abandon the handshake cleanly.
Requires the session's :handshake_write phase. Ownership, closed/unknown
handles, and a wrong handshake turn raise Decibel.SessionError before any
state change.
@spec handshake_hash(session()) :: handshake_hash() | nil
Returns the handshake hash unique to the established session.
The hash is 32 bytes for SHA256 and BLAKE2s, or 64 bytes for SHA512
and BLAKE2b. Returns nil if the handshake is not yet completed.
Examples
iex> initiator = Decibel.new("Noise_NN_25519_ChaChaPoly_SHA256", :ini)
iex> responder = Decibel.new("Noise_NN_25519_ChaChaPoly_SHA256", :rsp)
iex> Decibel.handshake_encrypt(initiator) |> then(&Decibel.handshake_decrypt(responder, &1))
""
iex> Decibel.handshake_encrypt(responder) |> then(&Decibel.handshake_decrypt(initiator, &1))
""
iex> match?(<<_::32-bytes>>, Decibel.handshake_hash(initiator))
true
iex> Decibel.handshake_hash(initiator) == Decibel.handshake_hash(responder)
true
iex> {Decibel.close(initiator), Decibel.close(responder)}
{:ok, :ok}
iex> initiator = Decibel.new("Noise_NN_448_AESGCM_BLAKE2b", :ini)
iex> responder = Decibel.new("Noise_NN_448_AESGCM_BLAKE2b", :rsp)
iex> Decibel.handshake_encrypt(initiator) |> then(&Decibel.handshake_decrypt(responder, &1))
""
iex> Decibel.handshake_encrypt(responder) |> then(&Decibel.handshake_decrypt(initiator, &1))
""
iex> match?(<<_::64-bytes>>, Decibel.handshake_hash(initiator))
true
iex> Decibel.handshake_hash(initiator) == Decibel.handshake_hash(responder)
true
iex> {Decibel.close(initiator), Decibel.close(responder)}
{:ok, :ok}This accessor is valid during either handshake turn and transport. Invalid
ownership or a closed/unknown handle raises Decibel.SessionError.
Deprecated alias for handshake_complete?/1.
Scheduled for removal in Decibel 2.0.
@spec new(String.t(), role(), key_material(), [option()]) :: session()
Start a new handshake.
The caller should provide a protocol name
and the role the caller will play in the protocol. The caller must provide all keys
required by the protocol, including local static keys first used by later handshake
messages. These are normally static keys or pre-shared keys (PSKs). Local ephemeral
keys for ordinary handshakes are
generated internally when their outbound e token is processed. The list of provided
keys should be identified as follows:
Before constructing a session, review Authentication and key handling and Negotiation and rollback.
:s: the party's public-private static key pair as a tuple. It is required whenever the selected role uses a static key anywhere in the fully modified pattern.:rs: the peer's public static key as a binary, only when the peer has a static pre-message.:e: only for a fallback handshake where the caller sent the failed handshake's original ephemeral; the caller's public-private ephemeral key pair as a tuple.:re: only for a fallback handshake where the peer sent the failed handshake's original ephemeral; the peer's ephemeral public key as a binary.:psks: a list of pre-shared symmetric keys (as binaries), exactly one 32-byte key for eachpskNmodifier.:prologue: any prologue data represented as iodata.
Public and private DH values must each have the exact length required by the
selected DH function: 32 bytes for 25519 or 56 bytes for 448.
The only supported option is :swap, whose value must be :ini or :rsp and
defaults to :ini. For interactive handshakes, the named role uses the first
key returned by Noise Split() as its outbound key, and the other role uses
that key as its inbound key. Both peers must use the same value. Noise Pipes
fallback uses swap: :rsp because the responder sends the first fallback
message. This option never reverses the fixed direction of a one-way
handshake.
Ephemeral keypairs belong to exactly one protocol run. They must never be shared across sessions, processes, or protocol names. The fallback inputs above reuse a key within the same compound-protocol run; they do not make general reuse safe.
Protocol names are limited to 255 bytes and must use the canonical Noise
syntax. Modifiers are applied from left to right, so pskN after fallback
indexes the remaining handshake messages. PSK modifiers whose relative order
does not affect the resulting pattern must be sorted alphabetically, as
required by Noise section 8.1.
Raises ArgumentError for malformed or unsupported protocol names, invalid
or non-canonical modifiers, impossible PSK placements, PSK lists that do not
contain exactly one 32-byte key per modifier, missing or malformed static key
material, invalid prologue iodata, and invalid options. It also raises
ArgumentError for caller-supplied ephemeral keys outside their role-specific
fallback pre-message or with lengths that do not match the selected DH
function. Validation completes before any session state is stored.
Returns an opaque session handle representing the handshake. The calling process owns the session for its lifetime; see Ownership and lifetime.
Get the current nonce value of the specified cipher.
Connectionless senders should read the outbound nonce immediately before
calling encrypt/3 and send that value with the ciphertext. See
Connectionless Transports for the replay
protection the recipient must provide, and
Nonces, replay protection, and rekeying
for the safe-use requirements.
After the final usable nonce, 2^64 - 2, is consumed, this returns the
reserved value 2^64 - 1 to indicate that the channel is exhausted.
Raises Decibel.TransportDirectionError before any state change if the
selected direction is not permitted by a one-way handshake.
Requires the :transport phase. Invalid ownership, a closed/unknown handle,
or use during the handshake raises Decibel.SessionError.
@spec rekey(session(), :in | :out) :: :ok
Rekey the inbound or outbound channel of the session.
Returns :ok after replacing the selected key.
Noise rekeying changes the selected channel's key but does not reset its nonce. Applications must coordinate rekeying with the peer and continue the existing counter. For connectionless transports, retain the corresponding replay window as well; delayed messages encrypted under the old key cannot be decrypted after rekeying.
See Nonces, replay protection, and rekeying for the application responsibilities around this operation.
Raises Decibel.TransportDirectionError before any state change if the
selected direction is not permitted by a one-way handshake.
Requires the :transport phase. Invalid ownership, a closed/unknown handle,
or use during the handshake raises Decibel.SessionError before any state
change.
Get the remote (static) key if available.
A returned key is protocol output, not a trust decision. Authenticate it according to Authentication and key handling.
This accessor is valid during either handshake turn and transport. Invalid
ownership or a closed/unknown handle raises Decibel.SessionError.
@spec set_nonce(session(), :in | :out, usable_nonce()) :: :ok
Set the current value of nonce for the specified cipher.
Returns :ok after selecting the nonce.
Danger: low-level nonce control
This function does not provide replay protection. Applications selecting
inbound nonces must reject every nonce that has already authenticated and
must record a nonce only after successful decryption. Applications should
normally read outbound nonces with nonce/2; moving an outbound nonce
backwards is rejected because reusing a nonce with the same key is a
catastrophic AEAD failure.
The nonce must be an integer from 0 through 2^64 - 2.
An inbound nonce may be selected in any order. An outbound nonce may remain unchanged or move forward, but cannot move backwards. A rejected operation leaves session state unchanged.
Raises Decibel.TransportDirectionError before any state change if the
selected direction is not permitted by a one-way handshake.
Raises Decibel.NonceError without changing state if the nonce is outside
the usable range or would move the outbound channel backwards.
See Nonces, replay protection, and rekeying before using this low-level operation.
Requires the :transport phase. Invalid ownership, a closed/unknown handle,
or use during the handshake raises Decibel.SessionError before any state
change.