Aranya Fast Channels provide high-throughput encryption for application data, with keys managed by Aranya’s policy system.
This is the path data takes through the system:
application plaintext <->
user library <->
fast channel seal <->
ciphertext <->
user preferred transport <->
ciphertext <->
user library <->
fast channel open <->
application plaintext
Data starts as plaintext in the application layer. The user library will encrypt/decrypt the data with fast channel open/seal operations. Ciphertext fast channel data is sent to other peers by the user library via the user’s transport mechanism of choice. AFC is transport-agnostic - applications can use TCP, QUIC, UDP, or any other transport for the encrypted data.
Channel creation is initiated through Aranya policy actions. When a policy action creates a channel, Aranya creates an ephemeral command that emits effects (AfcBidiChannelCreated or AfcBidiChannelReceived) that are processed by an AFC handler. The handler extracts channel keys from these effects and stores them in AFC state where the client can access them.
This is the path an ephemeral command takes through the system:
aranya ->
daemon (daemon writes channel keys to shm, user library reads them) ->
Unix domain socket api ->
user library ->
fast channel ctrl message ->
user provided transport ->
peer user library ->
peer Unix domain socket api ->
peer daemon ->
peer aranya ->
peer daemon writes shm channel keys ->
peer user library reads shm channel keys
AFC relies on POSIX shared memory API in order to share channel data between the Aranya client and Aranya daemon. The Aranya client only reads channel data while the Aranya daemon can read and modify the shared channel data.
The daemon may modify channel data when the client invokes one of the channel APIs or when the delete method on a channel object is called.
Note: This is the “afc” section of the Aranya daemon config
[afc]
# Determines whether AFC is enabled
enable = true
# Path to the shared memory object. See https://man7.org/linux/man-pages/man7/shm_overview.7.html
shm_path = "/afc"
# Maximum number of channels AFC should support
max_chans = 100
When the Aranya daemon starts up, it will reuse the shared memory object at shm_path if it exists and is valid. Otherwise, a new shared memory object will be created and initialized to a default state.
The shared memory path is transmitted from the Aranya daemon to the Aranya client via UDS IPC, allowing the client to create its own view of the shared memory.
Channels are locally identified by a channel_id which is a 32-bit integer.
The Aranya daemon generates channel IDs by incrementing a monotonic counter.
This provides direct lookup without hash truncation.
There is a one-to-many relationship between label_id and channel_id. In other words, a channel_id is associated with a single label_id but a label_id can be associated with many channels.
AFC supports bidirectional and unidirectional channels:
Bidirectional: Both devices can encrypt and decrypt. Each side has different seal/open keys that correspond to each other.
Unidirectional: The sender encrypts (seal_only_key), while the receiver decrypts (open_only_key).
Channel type is determined by the policy action used to create it.
**Note: This is an overview of the relevant Public APIs for the aranya-fast-channels crate.
add(channel_id, label_id, keys) -
Creates a new entry for a channel.remove(channel_id) -
Removes an existing channelseal(channel_id, label_id, ciphertext_buffer, plaintext) -> Header -
Encrypts plaintext for the specified channel.
Returns the header which includes channel metadata, like the AFC protocol version.open(channel_id, label_id, plaintext_buffer, ciphertext) -> sequence_number -
Decrypts ciphertext from the peer. Returns the sequence number.
N.B. the label_id given as input is compared against the label_id associated with the channel.Channel creation happens through policy actions:
action create_afc_bidi_channel(peer_id id, label_id id) {
let channel = afc::create_bidi_channel(peer_id, label)
publish AfcBidiChannelCreated { ... }
}
action create_afc_uni_channel(sender_id id, receiver_id id, label_id id) {
let channel = afc::create_bidi_channel(peer_id, label)
publish AfcUniChannelCreated { ... }
}
Applications handle transport - AFC only does encryption/decryption.
When channels are created, Aranya emits effects:
AfcBidiChannelCreated - Channel author receives this with keysAfcBidiChannelReceived - Peer receives this with corresponding keysAfcUniChannelCreated/Received - For unidirectional channelsThe AFC handler processes these effects to install keys in AFC state.
See Aranya Client APIs for the high-level client APIs. See Channel Types for the types of channel objects.
Create*Channel*(..) to create a channel object for the author and a ctrl message1
ctrl message via any transport (TCP, QUIC, etc.)ctrl message via transportReceiveCtrl(.., ctrl) to create their own channel objectChannel.seal(..) to encrypt data (Note: Assumes that an AfcBidiChannel or AfcSendChannel channel was created by calling Create*Channel*(..))Channel.open(..) to decrypt and get the sequence number (Note: Assumes that an AfcBidiChannel or AfcOpenChannel channel was created by calling ReceiveCtrl(..))This keeps AFC focused on encryption while letting apps choose their preferred transport.
Note: The user must keep track of (device -> channel object) pairs
The control message contains the serialized channel creation command, including the HPKE encapsulation that allows the peer device to derive the shared channel keys. Only the intended recipient can use this encapsulation with their private key to send/receive messages on the secure channel. See HPKE key derivation. ↩