NAME

Crypt::Age::Primitives - Low-level cryptographic primitives for age encryption

VERSION

version 0.001

SYNOPSIS

use Crypt::Age::Primitives;

# Generate random file key
my $file_key = Crypt::Age::Primitives->generate_file_key();

# X25519 key exchange
my ($pub, $priv) = Crypt::Age::Primitives->x25519_generate_keypair();
my $secret = Crypt::Age::Primitives->x25519_shared_secret($our_priv, $their_pub);

# Key derivation and wrapping
my $wrap_key = Crypt::Age::Primitives->derive_wrap_key($secret, $eph_pub, $rec_pub);
my $wrapped = Crypt::Age::Primitives->wrap_file_key($wrap_key, $file_key);
my $unwrapped = Crypt::Age::Primitives->unwrap_file_key($wrap_key, $wrapped);

# Payload encryption
my $payload_key = Crypt::Age::Primitives->derive_payload_key($file_key);
my $encrypted = Crypt::Age::Primitives->encrypt_payload($payload_key, $plaintext);
my $decrypted = Crypt::Age::Primitives->decrypt_payload($payload_key, $encrypted);

# Header MAC
my $mac = Crypt::Age::Primitives->compute_header_mac($file_key, $header_bytes);

DESCRIPTION

This module provides low-level cryptographic primitives for age encryption. It wraps functions from CryptX and implements the age-specific key derivation and payload encryption schemes.

This is an internal module used by Crypt::Age. Most users should use the high-level interface provided by Crypt::Age instead.

Cryptographic Primitives Used

  • X25519 - Key exchange (Curve25519 Diffie-Hellman)

  • ChaCha20-Poly1305 - AEAD encryption

  • HKDF-SHA256 - Key derivation

  • HMAC-SHA256 - Header MAC

generate_file_key

my $file_key = Crypt::Age::Primitives->generate_file_key();

Generates a random 16-byte file key using a cryptographically secure PRNG.

The file key is used to encrypt the payload and is itself encrypted for each recipient.

x25519_generate_keypair

my ($public_bytes, $private_bytes) = Crypt::Age::Primitives->x25519_generate_keypair();

Generates a new X25519 keypair. Returns raw 32-byte public and private keys.

Note: For generating age-encoded keypairs, use "generate_keypair" in Crypt::Age::Keys instead.

x25519_shared_secret

my $shared_secret = Crypt::Age::Primitives->x25519_shared_secret($our_private, $their_public);

Performs X25519 key exchange to compute a shared secret.

Parameters are raw 32-byte keys. Returns a 32-byte shared secret.

derive_wrap_key

my $wrap_key = Crypt::Age::Primitives->derive_wrap_key(
    $shared_secret,
    $ephemeral_public,
    $recipient_public
);

Derives a wrapping key from an X25519 shared secret using HKDF-SHA256.

The salt is ephemeral_public || recipient_public (concatenated). The info string is "age-encryption.org/v1/X25519".

Returns a 32-byte key suitable for wrapping the file key.

wrap_file_key

my $wrapped_key = Crypt::Age::Primitives->wrap_file_key($wrap_key, $file_key);

Wraps a 16-byte file key using ChaCha20-Poly1305 with a zero nonce.

Returns a 32-byte value: 16 bytes ciphertext + 16 bytes authentication tag.

unwrap_file_key

my $file_key = Crypt::Age::Primitives->unwrap_file_key($wrap_key, $wrapped_key);

Unwraps a wrapped file key using ChaCha20-Poly1305.

Dies if authentication fails. Returns the 16-byte file key on success.

derive_payload_key

my $payload_key = Crypt::Age::Primitives->derive_payload_key($file_key, $nonce);

Derives a 32-byte payload encryption key from the file key and nonce using HKDF-SHA256.

The nonce (16 bytes) is used as the salt, and "payload" is the info string.

generate_payload_nonce

my $nonce = Crypt::Age::Primitives->generate_payload_nonce();

Generates a random 16-byte nonce for payload encryption.

compute_header_mac

my $mac = Crypt::Age::Primitives->compute_header_mac($file_key, $header_bytes);

Computes HMAC-SHA256 MAC over the header bytes.

First derives a MAC key from the file key using HKDF with info string "header", then computes HMAC-SHA256 of the header. Returns 32 bytes.

encrypt_payload

my $ciphertext = Crypt::Age::Primitives->encrypt_payload($payload_key, $plaintext);

Encrypts the payload using ChaCha20-Poly1305 in chunked mode.

The plaintext is split into 64 KiB chunks. Each chunk is encrypted with a unique nonce derived from a counter and a final-chunk flag. Returns the concatenated encrypted chunks.

decrypt_payload

my $plaintext = Crypt::Age::Primitives->decrypt_payload($payload_key, $ciphertext);

Decrypts a chunked payload encrypted with encrypt_payload.

Dies if any chunk fails authentication. Returns the decrypted plaintext.

SEE ALSO

SUPPORT

Issues

Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-crypt-age/issues.

IRC

You can reach Getty on irc.perl.org for questions and support.

CONTRIBUTING

Contributions are welcome! Please fork the repository and submit a pull request.

AUTHOR

Torsten Raudssus <torsten@raudssus.de>

COPYRIGHT AND LICENSE

This software is copyright (c) 2026 by Torsten Raudssus.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.