Aranya Fast Channels (AFC) is a low latency, high throughput encryption engine that uses Aranya for key management and authorization.
Its primary concerns are throughput and latency. In general, its fast path should only add a handful of additional instructions on top of the underlying cryptography engine.
Encryption is scoped to a particular channel, which supports one-to-one communication in either a unidirectional or bidirectional manner.
"abc"
: A byte string containing the UTF-8 characters between
the double quotation marks ("
).concat(x0, ..., xN)
: The concatenation of byte strings.
concat(a, b, c) = abc
.EncryptionKey(u)
: The Aranya user’s EncryptionKey
.i2osp(n, w)
: Converts the unsigned (non-negative) integer n
to a w
-byte big-endian byte string.random(n)
: A uniform, pseudorandom byte string of n
bytes.(x0, ..., xN) = split(x)
: The reverse of concat
.UserId(u)
: The Aranya UserID for some user u
.ALG_Op(...)
: A cryptographic algorithm routine. E.g.,
AEAD_Seal(...)
, HPKE_OneShotSeal(...)
, etc.Conceptually, AFC implements this interface:
// Encrypts and authenticates `plaintext` for the channel
// (user, label).
fn encrypt(user, label, plaintext) -> ciphertext;
// Decrypts and authenticates `ciphertext` received from `user`.
fn decrypt(user, ciphertext) -> (label, plaintext);
As mentioned, a channel facilitates one-to-one communication. Logically, it is identified by a (user1, user2, label) tuple where user1 and user2 are Aranya users and label is an identifier that both users have been granted permission to use.
The label binds a channel to a set of Aranya policy rules, ensuring that both channel users meet some specified criteria.
Note: For performance reasons, users and labels are mapped to 32-bit integers.
Bidirectional channels allow both users to encrypt and decrypt data. Generally speaking, they’re the default channel type.
Each bidirectional channel has two unique symmetric AEAD keys, (k1, k2), called the ChannelKeys. One side of the channel uses k1 for encryption and k2 for decryption. The other side uses the k2 for encryption and k1 for decryption. The key used for encryption is referred to as the SealKey and the key used for decryption is referred to as the OpenKey.
ChannelKeys are derived using HPKE’s Secret Export API.
For domain separation purposes, the key derivation scheme includes both UserIDs. Additionally, in order to prevent duplicate ChannelKeys (from a buggy CSPRNG), it mixes in the ID of the command that created the channel. (Command IDs are assumed to be unique; for more information, see the Aranya spec.)
The key derivation scheme is as follows:
// `parent_cmd_id` is the parent command ID.
fn NewChannelKeys(us, peer, parent_cmd_id, label) {
if UserId(us) == UserId(peer) {
raise SameIdError
}
suite_id = concat(aead_id, kdf_id, signer_id, ...)
info = concat(
"AfcChannelKeys",
suite_id,
engine_id,
parent_cmd_id,
UserId(us),
UserId(peer)
i2osp(label),
)
(enc, ctx) = HPKE_SetupSend(
mode=mode_auth,
skS=sk(EncryptionKey(us)), // our private key
pkR=pk(EncryptionKey(peer)), // the peer's public key
info=info,
)
SealKey = HPKE_ExportSecret(ctx, UserId(peer))
OpenKey = HPKE_ExportSecret(ctx, UserId(us))
// `enc` is sent to the other user.
// `seal_key` and `open_key` are provided to AFC.
return (enc, (SealKey, OpenKey))
}
// `parent_cmd_id` is the parent command ID.
fn DecryptChannelKeys(enc, us, peer, parent_cmd_id, label) {
if UserId(us) == UserId(peer) {
raise SameIdError
}
suite_id = concat(aead_id, kdf_id, signer_id, ...)
info = concat(
"AfcChannelKeys",
suite_id,
engine_id,
parent_cmd_id,
// Note how these are swapped.
UserId(peer)
UserId(us),
i2osp(label),
)
ctx = HPKE_SetupRecv(
mode=mode_auth,
enc=enc,
pkS=pk(EncryptionKey(peer)), // the peer's public key
skR=sk(EncryptionKey(us)), // our private key
info=info,
)
// Remember, these are the reverse of `NewChannelKeys`.
SealKey = HPKE_ExportSecret(ctx, UserId(peer))
OpenKey = HPKE_ExportSecret(ctx, UserId(us))
return (seal_key, open_key)
}
Unidirectional channels allow one user to encrypt and one user to decrypt.
Each unidirectional channel has one unique symmetric AEAD key. The side that encrypts calls this the SealOnlyKey and the side that decrypts calls this the OpenOnlyKey.
The SealOnlyKey/OpenOnlyKey is derived using HPKE’s Secret Export API.
For domain separation purposes, the key derivation scheme includes both UserIDs. Additionally, in order to prevent duplicate keys (from a buggy CSPRNG), it mixes in the ID of the command that created the channel. (Command IDs are assumed to be unique; for more information, see the Aranya spec.)
The key derivation scheme is as follows:
// `seal_id` is the user that is allowed to encrypt.
// `open_id` is the user that is allowed to decrypt.
// `parent_cmd_id` is the parent command ID.
fn NewSealOnlyKey(seal_id, open_id, parent_cmd_id, label) {
if seal_id == open_id {
raise SameIdError
}
suite_id = concat(aead_id, kdf_id, signer_id, ...)
info = concat(
"AfcUnidirectionalKey",
suite_id,
engine_id,
parent_cmd_id,
seal_id,
open_id,
i2osp(label),
)
(enc, ctx) = HPKE_SetupSend(
mode=mode_auth,
skS=sk(EncryptionKey(us)), // our private key
pkR=pk(EncryptionKey(peer)), // the peer's public key
info=info,
)
SealOnlyKey = HPKE_ExportSecret(ctx, "unidirectional key")
// `enc` is sent to the other user.
// `SealOnlyKey` is provided to AFC.
return (enc, SealOnlyKey)
}
// `seal_id` is the user that is allowed to encrypt.
// `open_id` is the user that is allowed to decrypt.
// `parent_cmd_id` is the parent command ID.
fn DecryptOpenOnlyKey(enc, us, peer, parent_cmd_id, label) {
if UserId(us) == UserId(peer) {
raise SameIdError
}
suite_id = concat(aead_id, kdf_id, signer_id, ...)
info = concat(
"AfcUnidirectionalKey",
suite_id,
engine_id,
parent_cmd_id,
seal_id,
open_id,
i2osp(label),
)
ctx = HPKE_SetupRecv(
mode=mode_auth,
enc=enc,
pkS=pk(EncryptionKey(peer)), // the peer's public key
skR=sk(EncryptionKey(us)), // our private key
info=info,
)
return HPKE_ExportSecret(ctx, "unidirectional key")
}
Outside of key derivation, the remaining cryptography is identical for both channel types.
AFC encrypts each message with a uniformly random nonce generated by a CSPRNG.
fn Seal(user, label, SealKey, plaintext) {
header = concat(
i2osp(version, 4), // version is a constant
i2osp(user, 4),
i2osp(label, 4),
)
nonce = random(AEAD_nonce_len())
SealKey = FindSealKey(user, label)
ciphertext = AEAD_Seal(
key=SealKey,
nonce=nonce,
plaintext=plaintext,
ad=header,
)
// For performance reasons, the nonce and header are
// appended, instead of prepended.
return concat(ciphertext, nonce, header)
}
fn Open(user, label, ciphertext) {
// NB: while the header includes multiple fields, we only use
// the `label` since we already know everything else.
(ciphertext, nonce, header) = split(ciphertext);
(_, _, label) = split(header);
OpenKey = FindOpenKey(user, label)
plaintext = AEAD_Open(
key=OpenKey,
nonce=nonce,
ciphertext=ciphertext,
ad=header,
)
return plaintext
}
Each encryption key must not be used more than allowed by the underlying AEAD (i.e., it should respect the AEAD’s lifetime). The current specification does not require AFC to track how much a particular key is used. This will change in the future.
Briefly, AEAD encryption is a construction with four inputs:
K
N
that is unique for each unique (K, P)
tupleP
which will be encryptedA
that will be authenticated, but not
encryptedIt outputs a ciphertext C
which is at least as long as P
.
AEAD decryption works in the inverse manner. For formal and
more comprehensive documentation, see RFC 5116.
The requirements on the chosen AEAD are more restrictive than RFC 5116. Specifically, the cipher must:
Examples of AEAD algorithms that fulfill these requirements include AES-256-GCM, ChaCha20-Poly1305, and Ascon. It is highly recommended to use a nonce misuse-resistant AEAD, like AES-GCM-SIV.
A committing AEAD is an AEAD that binds the authenticator to one or more of the AEAD inputs. For more information, see Efficient Schemes for Committing Authenticated Encryption.
An extract-then-expand Key Derivation Function (KDF) as formally defined in section 3 of HKDF.
The KDF must:
Note: It does not need to be suitable for deriving keys from passwords. In other words, it does not need to be a “slow” KDF like PBKDF2.
Hybrid Public Key Encryption (HPKE) per RFC 9180.