Aranya Documentation An overview of the Aranya project

AFC Cryptography

Overview

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.

Notation

  • "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.

Design

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

Bidirectional channels allow both users to encrypt and decrypt data. Generally speaking, they’re the default channel type.

Cryptography

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.

Key Derivation

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

Unidirectional channels allow one user to encrypt and one user to decrypt.

Cryptography

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.

Key Derivation

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")
}

Cryptography

Outside of key derivation, the remaining cryptography is identical for both channel types.

Message Encryption

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
}

Key Usage

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.

Algorithms

AEAD

Briefly, AEAD encryption is a construction with four inputs:

  1. uniformly random key K
  2. nonce N that is unique for each unique (K, P) tuple
  3. plaintext P which will be encrypted
  4. associated data A that will be authenticated, but not encrypted

It 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:

  • Have at least a 128-bit security level for confidentiality.
  • Have at least a 128-bit security level for authenticity.
  • Have a minimum key size of 16 octets (128 bits).
  • Accept plaintexts up to 2³² - 1 octets (2³⁵ - 8 bits) long.
  • Accept associated data up to 2³² - 1 (2³⁵ - 8 bits) octets long.

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.

Committing AEAD

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.

KDF

An extract-then-expand Key Derivation Function (KDF) as formally defined in section 3 of HKDF.

The KDF must:

  • Have a security level of at least 128 bits.
  • Extract a PRK at least 128 bits long.
  • Expand a PRK into a key at least 512 bits long.

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.

HPKE

Hybrid Public Key Encryption (HPKE) per RFC 9180.