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 device’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.DeviceId(u): The Aranya DeviceID for some device 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
// (device, label).
fn encrypt(device, label, plaintext) -> ciphertext;
// Decrypts and authenticates `ciphertext` received from `device`.
fn decrypt(device, ciphertext) -> (label, plaintext);
As mentioned, a channel facilitates one-to-one communication. Logically, it is identified by a (device1, device2, label) tuple where device1 and device2 are Aranya devices and label is an identifier that both devices have been granted permission to use.
The label binds a channel to a set of Aranya policy rules, ensuring that both channel devices meet some specified criteria.
Bidirectional channels allow both devices 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 DeviceIDs. 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_id) {
if DeviceId(us) == DeviceId(peer) {
raise SameIdError
}
info = concat(
"AfcBidiKeys-v1",
parent_cmd_id,
DeviceId(peer),
DeviceId(us),
label_id,
oids
)
// oids include AEAD, KDF, and HPKE algorithm OIDs for contextual binding
(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, "bidi response key")
OpenKey = HPKE_ExportSecret(ctx, "bidi response key")
SealBaseNonce = HPKE_ExportSecret(ctx, "bidi response base_nonce")
OpenBaseNonce = HPKE_ExportSecret(ctx, "bidi response base_nonce")
// `enc` is sent to the other device.
// Keys and base nonces are provided to AFC.
return (enc, (SealKey, OpenKey, SealBaseNonce, OpenBaseNonce))
}
// `parent_cmd_id` is the parent command ID.
fn DecryptChannelKeys(enc, us, peer, parent_cmd_id, label_id) {
if DeviceId(us) == DeviceId(peer) {
raise SameIdError
}
info = concat(
"AfcBidiKeys-v1",
parent_cmd_id,
// Note how these are swapped.
DeviceId(us),
DeviceId(peer),
label_id,
oids
)
// oids include AEAD, KDF, and HPKE algorithm OIDs for contextual binding
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, "bidi response key")
OpenKey = HPKE_ExportSecret(ctx, "bidi response key")
SealBaseNonce = HPKE_ExportSecret(ctx, "bidi response base_nonce")
OpenBaseNonce = HPKE_ExportSecret(ctx, "bidi response base_nonce")
return (SealKey, OpenKey, SealBaseNonce, OpenBaseNonce)
}
Unidirectional channels allow one device to encrypt and one device 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 DeviceIDs. 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 device that is allowed to encrypt.
// `open_id` is the device that is allowed to decrypt.
// `parent_cmd_id` is the parent command ID.
fn NewSealOnlyKey(seal_id, open_id, parent_cmd_id, label_id) {
if seal_id == open_id {
raise SameIdError
}
info = concat(
"AfcUniKey-v1",
parent_cmd_id,
seal_id,
open_id,
label_id,
oids
)
// oids include AEAD, KDF, and HPKE algorithm OIDs for contextual binding
(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")
SealBaseNonce = HPKE_ExportSecret(ctx, "unidirectional base_nonce")
// `enc` is sent to the other device.
// `SealOnlyKey` and base nonce are provided to AFC.
return (enc, SealOnlyKey, SealBaseNonce)
}
// `seal_id` is the device that is allowed to encrypt.
// `open_id` is the device that is allowed to decrypt.
// `parent_cmd_id` is the parent command ID.
fn DecryptOpenOnlyKey(enc, us, peer, parent_cmd_id, label_id) {
if DeviceId(us) == DeviceId(peer) {
raise SameIdError
}
info = concat(
"AfcUniKey-v1",
parent_cmd_id,
seal_id,
open_id,
label_id,
oids
)
// oids include AEAD, KDF, and HPKE algorithm OIDs for contextual binding
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,
)
OpenOnlyKey = HPKE_ExportSecret(ctx, "unidirectional key")
OpenBaseNonce = HPKE_ExportSecret(ctx, "unidirectional base_nonce")
return (OpenOnlyKey, OpenBaseNonce)
}
Outside of key derivation, the remaining cryptography is identical for both channel types.
AFC encrypts each message with a deterministic nonce derived from a base nonce and sequence number.
type ChannelId = u32
fn Seal(channel_id, label_id, SealKey, SealBaseNonce, sequence, plaintext) {
header = concat(
i2osp(version, 4), // version is a constant
label_id,
)
nonce = xor(SealBaseNonce, i2osp(sequence, AEAD_nonce_len()))
SealKey = FindSealKey(channel_id)
ciphertext = AEAD_Seal(
key=SealKey,
nonce=nonce,
plaintext=plaintext,
ad=header,
)
return (ciphertext, sequence)
}
fn Open(channel_id, label_id, OpenKey, OpenBaseNonce, ciphertext) {
(cipher_text, header) = split(ciphertext);
sequence = parse(header)
nonce = xor(OpenBaseNonce, i2osp(sequence, AEAD_nonce_len()))
// Find the 'open' key and the label_id associated with this channel
(OpenKey, label_id_from_channel) = FindOpenKey(channel_id)
if label_id != label_id_from_channel {
return Err(InvalidLabel)
}
ad = concat (
i2osp(version, 4), // version is a constant
label_id,
)
plaintext = AEAD_Open(
key=OpenKey,
nonce=nonce,
ciphertext=ciphertext,
ad=ad,
)
return (plaintext, sequence)
}
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.