c2sp.org/age, age-encryption.org/v1
age is a modern file encryption format with multiple pluggable recipients, and seekable streaming encryption.
ABNF syntax follows RFC 5234 and RFC 7405 and references the core rules in RFC 5234, Appendix B.1.
The base64 encoding used throughout is the standard Base 64 encoding specified
in RFC 4648, Section 4, without = padding characters (sometimes referred
to as "raw" or "unpadded" base64). Encoders MUST generate canonical base64
according to RFC 4648, Section 3.5, and decoders MUST reject non-canonical
encodings and encodings ending with = padding characters.
Keys derived with HKDF-SHA-256 are produced by applying HKDF-Extract with the specified salt followed by HKDF-Expand with the specified info according to RFC 5869. The hash used with HKDF in this specification is always SHA-256. The length of the output keying material is always 32 bytes.
ChaCha20-Poly1305 is the AEAD encryption function from RFC 7539.
Bech32 is as specified in BIP173, but without length limits on the data part. Note that Bech32 strings can only be all uppercase or all lowercase, but the checksum is always computed over the lowercase string.
|| denotes concatenation. 0x followed by two hexadecimal characters denotes
a byte value in the 0-255 range. [:N] denotes truncation to the first N
bytes of a byte string.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 RFC 2119 RFC 8174 when, and only when, they appear in all capitals, as shown here.
An age file is composed of two parts: a textual header that carries the file key, and a binary payload encrypted with it. Overall, age files MUST be treated as binary, and are not malleable without knowledge of the file key.
age files MAY use the extension .age, in both their binary and
armored formats.
Each file is encrypted with a 128-bit symmetric file key.
The file key MUST be generated as 16 bytes of CSPRNG output. It MUST NOT be reused across multiple files.
The textual file header wraps the file key for one or more recipients, so that it can be unwrapped by one of the corresponding identities. It starts with a version line, followed by one or more recipient stanzas, and ends with a MAC.
age-encryption.org/v1
-> X25519 XEl0dJ6y3C7KZkgmgWUicg63EyXJiwBJW8PdYJ/cYBE
qRS0AMjdjPvZ/WT08U2KL4G+PIooA3hy38SvLpvaC1E
--- HK2NmOBN9Dpq0Gw6xMCuhFcQlQLvZ/wQUi/2scLG75s
Note that each section of the header can be parsed by looking at its first three characters, and it ends either at the next newline (for version and MAC lines) or at the first line shorter than 64 columns (for stanzas).
The version line always starts with "age-encryption.org/", is followed by an
arbitrary version string, and ends with a line feed (0x0A).
version-line = %s"age-encryption.org/" version LF
version = 1*VCHAR
This document only specifies the v1 format. Anything after the end of the
version line may change in future versions.
A recipient stanza starts with ->, followed after a space by one or more space-separated
arguments, and a base64-encoded body wrapped at 64 columns. The body MUST end
with a line shorter than 64 characters, which MAY be empty.
Each recipient stanza wraps the same file key independently. Identity implementations are provided the full set of stanzas and recognize those addressed to them from their arguments. Identity implementations MUST ignore unrecognized stanzas, unless they wish to require that the recipient type they implement is not mixed with other types.
It is RECOMMENDED that non-native recipient implementations use fully-qualified
names as the first stanza argument, such as example.com/enigma, to avoid
ambiguity and conflicts.
Recipient implementations MAY choose to include an identifier of the specific recipient (for example, a short hash of the public key) as an argument. Note that this sacrifices any chance of ciphertext anonymity and unlinkability.
The final header line starts with --- and is followed after a space by the
base64-encoded MAC of the header. The MAC is computed with HMAC-SHA-256 (see
RFC 2104) over the whole header up to and including the --- mark
(excluding the space following it).
The HMAC key is computed as follows:
HMAC key = HKDF-SHA-256(ikm = file key, salt = empty, info = "header")
The following is the ABNF definition of the v1 file header.
header = v1-line 1*stanza end
v1-line = %s"age-encryption.org/v1" LF
end = "--- " 43base64char LF
base64char = ALPHA / DIGIT / "+" / "/"
stanza = arg-line *full-line final-line
arg-line = "-> " argument *(SP argument) LF
argument = 1*VCHAR
full-line = 64base64char LF
final-line = *63base64char LF
The binary payload encrypts the file body and starts immediately after the header. It begins with a 16-byte nonce generated by the sender from a CSPRNG. A new nonce MUST be generated for each file.
The payload key is computed as follows:
payload key = HKDF-SHA-256(ikm = file key, salt = nonce, info = "payload")
The payload is split in chunks of 64 KiB, and each of them is encrypted with ChaCha20-Poly1305, using the payload key and a 12-byte nonce composed as follows: the first 11 bytes are a big endian chunk counter starting at zero and incrementing by one for each subsequent chunk; the last byte is 0x01 for the final chunk and 0x00 for all preceding ones. The final chunk MAY be shorter than 64 KiB but MUST NOT be empty unless the whole payload is empty.
This is a STREAM variant from Online Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance. It is similar to those used by Tink and Miscreant, but it doesn't prefix the AEAD nonce with key material as the payload key is 256 bits (enough even to provide a security margin in the multi-target setting) and derived from both file key and nonce.
The payload can be streamed by decrypting or encrypting one chunk at a time. Streaming decryption MUST signal an error if the end of file is reached without successfully decrypting a final chunk.
The payload can be seeked by jumping ahead in chunk increments, and decrypting the whole chunk that contains the seeked position. Seeking relatively to the end of file MUST first decrypt and verify that the last chunk is a valid final chunk.
The payload MUST NOT be modified without re-encrypting it as a new file with a fresh nonce.
This document specifies five native age recipient types: a hybrid post-quantum asymmetric encryption type based on X-Wing, an asymmetric encryption type based on X25519, a passphrase encryption type based on scrypt, and two tagged recipient types based on ML-KEM and P-256 ECDH for hardware keys.
An MLKEM768-X25519 identity is generated as
identity = read(CSPRNG, 32)
and encoded as Bech32 with HRP AGE-SECRET-KEY-PQ-.
AGE-SECRET-KEY-PQ-1XX76JRALNLXDMEW0CRK45QMCCH4X06SE84UN3VPM33W6HWDX0H3SK3ZQFR
The corresponding recipient is computed as
recipient = PrivateKeyToPublicKey(identity)
where PrivateKeyToPublicKey is as specified in filippo.io/hpke-pq for the MLKEM768-X25519 hybrid HPKE KEM.
The recipient is encoded as Bech32 with HRP age1pq.
age1pq1x34nzsvr0rxjsgdn8zgyhfe8j7ceq5r9rdelkjuh3y235jzxshfg87pzf5zrqtzdxz95paef6caq5aapdmwjjqpjfdyxnzr2zampc3uxy0dg4z2n2gm9su72p0pc3u0jvev55l694v78snxg3yzvcl7yda0eyytqj6a0ec477lnhcy5hzpz4zq3pxanve4cn62gqj3pjy5lqj9c6kyj4v2z8alktn8zh99970x79gjkv7522hv9kfz35zsnxhsx8wwtmu9cy3ftzjgwcp4sshn3llnylnpdsyz5jm72vefv4x5vfwytrefxg4wq3mv42wcrvkj742479zrxzpvp2p3e9fed9f0739vcu80r7ma28qfhnvlv4gfzel9q654dj3zmuvvz893azhxdvs9fxd0r7jzchzcfcs5mkyyjxhw0n2z6dvp9yn9qfdp29h0azxqyjw6v7fhyuzj7zel0uq6j9rd7wgrpz7mf5dnj43jwsgvrc8qcnhy7tu6dkdujuxzkp9xj43xe8h92ktre2a3u3s8mm5mrp9nr9pwkgtz4mdlq9hgn4fps4k57ff6wddn2fy23t47sm20r8km8sd2pcyyafnet8f0dajsrlyjeah4n3mssr6aseevuuskdvq5lzguyvpgwpta742c6698vgutzqgny8usfg0w2he7kq5vyxjd0f9hqg8xk26y9e4th0gezq92q4cpp5p2y9hf5f2cje5l0c3sa3a2qxmm38pxxvhxh99yzmfz0zk7r2s64nnwjhkfgfr3gf8xnmppcgmaykvh5sh6g7vk9790rf8ws0axmr2t7z8aae5fq2029uvcn2ghgt4fu4wgwdc0k0cz52qkvwmuzj8p8k5jgf3xzk5zmrkavjekjrpeq408xz3zxazwkc6tyfmhayrkfpjhwtz5mp8j8guqe43k2q6m2kte03vrw27y3wmqyu5etmt9dnkwcnnpmu9gz9dekfhdevf42ucshphnrk38ra6hx8w5f8q5ru0xdhrjxmwqf6cused7zc5xvq43r0zscjglpwlptpwydhqw64xz7ptjdyeyzpq2zkxtmzg29gzjpvzva4d3l0cenn9xs297wf4y4ukwrunf57xj6pm7nvrkwvtrt8hwcmgv8x7ajw7258ugf9wvkmk4052ekg87tw5vnx8nq2swyzv77v8yqlwsenvamr0zssknwts8rrhfuwj7ykysnq9jxy0uv3kuyt22djszjdtvpz6d0s0kwh8ryynddzud92emeyvvyqktd0jtj7rvvg5gch25v8smlvny3kvn5gagyz475ze2y6q466xqmz2n3hs77lddeqyta2nch5k2u5yacuk9ywnwfdzvyejnucz724hj77hrrmakm7pr3kxsrxq22ejexlud9fy2kdqmkg5yncz7jm5wv2qjk5w5kvcpqsry2yqffh2la52dxfjkjq5rzhjzeyn6dupn0qwtyv7s4lwg3xdarsdlwe2y3tujy480y7z39q259fzx6jhd2j0f5hagqpcpees7hzc2yrk5cy788uk3s7qvp5cpepx24gvws3m2g433exgwppnkjscec8qu4y9z9r7vccexjcjaen42245lmgmxmuavg9alej92322gvvyy2t6267v09ch64y0m53jff0vjj96s0ypk60hr3jw4myd6m5hpn3xjstx7tl2szhpr5qe8jj08ydjc4wy2rch2fhuy3pdfjax5awe9j99ly5hkntzz9fe5zatgjvzdd0kgtxs25njnajyf6ssekp7gelxquusn4pt25czh3scj68kq79wdn5tgm6yvm9nzavrg043x3msnygf8dweknw5jmqd0uvny6ttsn09508k0c55zfnegrm9efhxpfqdkmhh6gjtqmwze9pyyzk3tlhl53k2ykx3qheyty7saeq0d3fzv49zc0k
This recipient type is secure against future cryptographically-relevant quantum computers, so the same file SHOULD NOT be encrypted to both this recipent type and to other non-quantum-resistant recipient types.
To produce a mlkem768x25519 recipient stanza, the file key is encrypted with the HPKE SealBase function from RFC 9180, Section 6.1 with the following parameters:
- KEM: MLKEM768-X25519 from draft-ietf-hpke-pq-03/filippo.io/hpke-pq
- KDF: HKDF-SHA256
- AEAD: ChaCha20Poly1305
pkR = recipientinfo = "age-encryption.org/mlkem768x25519"aad = ""(empty)
It is then encoded as a recipient stanza with two arguments: the first is the
fixed string mlkem768x25519, and the second is the base64-encoded encapsulated
key enc from SealBase.
The body of the recipient stanza is the HPKE ciphertext from SealBase.
-> mlkem768x25519 U7E10Aon27j1oDrH7fP3B++SPROSnDVvQpBemzOM+ZcsojxJifUbeeYWejMYKNB+H3rlXNvgtDKfSCkt78oqncr3faY1JtMj0GQPBZ2g6t8o1cMzZgyLMepeVuTQKxLqnM6L48n8dJL2y0N3i7WSOV6csB79r9tcIs88Al9iCwVLpt74OzDXAIwD1QlCAvj75ZEnwtgP6Xr6s+wHg9hLD0OSSQOaTdCqzR5shQWyrjVs7GCqd/5WbNGYNaolPQJBu9wTNhHjP9XGCTv8iKXkYwXDYujInkF5hOjpCHE8Vu+9UeMI+l0GJAEervT51bwaf288GiqgapP5g4HF8V4P0+03B/SwXhvxxZpKjTU2OcFS8I9chtEZ7Ucw5bvPkC1lyjNnqQXZyc4JnAqbFQTlPGnwbEsm0UGE+v0FOxTBznIivHoPHV++5PuYpombiV/lfYy8BFx0Hk1igNI3iLNjlIg4MLXNWjN3VROlXIiSsWNXjr/Frh/OE87PXoaKhsDPWfv6akd9PxSDqJMQ4j8k11kCSOnnU8Rw4TSVnTpt7gfYzBULrMr7qDgyH/TYvp0o7mAq4IaraFJmTqm+LjfAJT4PCQOpnMNRHLyXZn6mtMUujNv7+zR/I9Vqi+bJdQBwNWdJcUQVarqx2zxV2rM7l+j0gsmxjxp5yAPyALzla7XzVvbboLMnHeM+CzN4GoB2PC4neVDSw7Dfs8vbm7f9CeDGvhjWrzcSCOsUCalvH1WkbpE0b9cE/TSJqwXFXEc8npTSQWH58RXDALNF3qCbW5LaboD4wCloClEakgljT0s0mTdqIwzj4xtdlX0S2dCtaV5MHQJiQHGwPvBSCiIsh8ZWJPwZ7Ron7xoWgNI0lXRqhKm7fvFNCzF+Ray2kOyKWHbsMy3a5Zx3yZxCGFwexAxgYfAH9HJ1raHi1XYGpNpBlDXg47U1y4uU7RBeBb4ewETv/B/p8sGW88tIY87dHM2xHjnntGXLvkCIkmjXGNULAVYHxDPJTygxSF7uTVPYCBUk5KeLnDVcgWF8p/wxigza47ptMtpVtm9ZrSEu9vzOnaTN3KO75RkkLuZszhXfmMZ46NQrdS7Y6duCiN501BnR5BUC4dX4iMxchix6aLN3pKv6UOEwwcx2aSF4Ib963MAtTPEjhqe+dt0VcTar/08uLWv1kQQueFaEGet4KQ975ZqaoU3PmZH9gqTOumX1EKf0Pnl0F3NMW+lEOlnCEce99zHBwBf7LV0pLNLDONBxpQySrZUQsNfhAq2Ktf5nG2hcGAMFMWIsEOuOyzEe+l2aHW4/ojOjU1Oqy4TLzVVRlWmPxQde7qKbvBN7CQhDqJHJA2nphQT/4DOdLqkwRiauMmnfx9FI3+hYnX25NpnyvM/Pt94vpsRkhcuttu4/G4xOYTNQd2YWjGwhypZ0oPSiriuTbhj6y2qm33jiwxvegeDz8CmAIhOEY2JoVpjjX9XzuNR2f5KTLmLzEkOcJQ
c/k+TjhBA6Je1DF8rxZhe2Jm8CN0zbkV9rre6nA8g98
The file key can be decrypted with OpenBase from HPKE with the same parameters
as above, and skR = identity.
The identity implementation MUST ignore any stanza that does not have
mlkem768x25519 as the first argument, and MUST otherwise reject any stanza
that has more or less than two arguments, or where the second argument is not a
canonical base64 encoding of a 1120-byte value. It MUST check that the body
length is exactly 32 bytes before attempting to decrypt it, to mitigate
partitioning oracle attacks.
An X25519 identity is generated as
identity = read(CSPRNG, 32)
and encoded as Bech32 with HRP AGE-SECRET-KEY-.
AGE-SECRET-KEY-1GFPYYSJZGFPYYSJZGFPYYSJZGFPYYSJZGFPYYSJZGFPYYSJZGFPQ4EGAEX
The corresponding recipient is computed as
recipient = X25519(identity, basepoint)
where X25519 is from RFC 7748, Section 5, and basepoint is the
Curve25519 base point from RFC 7748, Section 4.1.
The recipient is encoded as Bech32 with HRP age.
age1zvkyg2lqzraa2lnjvqej32nkuu0ues2s82hzrye869xeexvn73equnujwj
An X25519 recipient stanza has two arguments.
-> X25519 O6DLx/wDIawpUC978NSPjYvrfDtJVnZApXKp4FMPHCY
3aKjozt9agh7jGmvOKvR4iax41Wl4zj95MKK4X9JuWc
The first is the fixed string X25519 and the second is the base64-encoded
ephemeral share computed by the recipient implementation as follows:
ephemeral secret = read(CSPRNG, 32)
ephemeral share = X25519(ephemeral secret, basepoint)
A new ephemeral secret MUST be generated for each stanza and each file.
The body of the recipient stanza is computed by the recipient implementation as
salt = ephemeral share || recipient
info = "age-encryption.org/v1/X25519"
shared secret = X25519(ephemeral secret, recipient)
wrap key = HKDF-SHA-256(ikm = shared secret, salt, info)
body = ChaCha20-Poly1305(key = wrap key, plaintext = file key)
where the ChaCha20-Poly1305 nonce is fixed as 12 0x00 bytes.
The identity implementation MUST ignore any stanza that does not have X25519
as the first argument, and MUST otherwise reject any stanza that has more or
less than two arguments, or where the second argument is not a canonical base64
encoding of a 32-byte value. It MUST check that the body length is exactly 32
bytes before attempting to decrypt it, to mitigate partitioning oracle attacks.
The identity implementation computes the shared secret as follows:
shared secret = X25519(identity, ephemeral share)
If the shared secret is all 0x00 bytes, the identity implementation MUST abort.
Finally, it derives the key as above and decrypts the file key in the body.
The scrypt recipient and identity implementations encrypt and decrypt the file key with a provided passphrase.
An scrypt recipient stanza has three arguments.
-> scrypt ajMFur+EJLGaohv/dLRGnw 18
8SHBz/ldWnjyGFQqfjat6uNBarWqqEMDS7W8X7+Xq5Q
The first is the string scrypt, the second is a base64-encoded salt computed
by the recipient implementation as 16 bytes from a CSPRNG, and the third is the
base-two logarithm of the scrypt work factor in decimal.
A new salt MUST be generated for each stanza and each file.
The body is computed as
wrap key = scrypt(N = work factor, r = 8, p = 1, dkLen = 32,
S = "age-encryption.org/v1/scrypt" || salt, P = passphrase)
body = ChaCha20-Poly1305(key = wrap key, plaintext = file key)
where the ChaCha20-Poly1305 nonce is fixed as 12 0x00 bytes and scrypt is from RFC 7914.
The identity implementation MUST reject any scrypt stanza that has more or less
than three arguments, where the second argument is not a canonical base64 encoding of a
16-byte value, or where the third argument is not a decimal number composed of
only digits with no leading zeroes (%x31-39 *DIGIT in ABNF or ^[1-9][0-9]*$
in regular expression). The identity implementation SHOULD apply an upper limit
to the work factor, and it MUST check that the body length is exactly 32 bytes
before attempting to decrypt it, to mitigate partitioning oracle attacks.
An scrypt stanza, if present, MUST be the only stanza in the header. In other words, scrypt stanzas MAY NOT be mixed with other scrypt stanzas or stanzas of other types. This is to uphold an expectation of authentication that is implicit in password-based encryption. The identity implementation MUST reject headers where an scrypt stanza is present alongside any other stanza.
The tagged recipient types are designed for hardware keys, where decryption potentially requires user presence. With knowledge of the public key, it is possible to check if a stanza was addressed to a specific recipient before attempting decryption. (This offers less privacy than the default recipient types.) The tagged recipient types are based on HPKE, and use P-256 ECDH for compatiblity with existing hardware, optionally hybridized with ML-KEM-786 for quantum resistance.
This document only defines the recipient encodings, and does not define how the corresponding identities are generated or encoded. We expect these recipients to be used as the public side of hardware-specific plugin identities.
The non-hybrid recipient is a P-256 curve point serialized as 33 bytes with the
compressed Elliptic-Curve-Point-to-Octet-String conversion from SEC 1, Ver.
2 and encoded as Bech32 with HRP age1tag.
age1tag1qt8lw0ual6avlwmwatk888yqnmdamm7xfd0wak53ut6elz5c4swx2yqdj4e
The hybrid recipient is an ML-KEM-768 encapsulation key concatenated with an
uncompressed P-256 curve point (unlike the non-hybrid recipient), for a total
of 1249 bytes, and encoded as Bech32 with HRP age1tagpq.
age1tagpq1m3e4wvp6hzcrn9exhy0ae3xfx2sjymp594k3tg7j4dpmj922we65vtnmrt2pyallax8669zqkr2pmfchptr4n38kug2xmcmp3adk2lnjqu00x5kxz5pvhmrltvfh9wuq973pcx35cnq8syn9qd3tzpehgztl4xpzr3tpd67g8af9trnjpc05gh7wu536aq4qt2y8zhsm4tvrfpsfl36qs5fpzysnk3sp9w77qzeg49357xex40v4s2lvt620swyys7u8yxdcnu4rkkwxdmt55gsuc3h5c5swahnegjgqwc60hn085ec3sjztwm45l44y3j2at9t6v9zra4ek3kek6waecqm98yaxl37w0d2zra626nz63jdm5sg59w7lyptw83zm6fntd8d0x03a9z6h9prfgpygzar6zrxjcrt4cdctk2mhf95s4a6v4zklfd49xhpsaeujm57thx2x3e3hwzc86ftfhmq5mkxxz3d6r8ws24xj4qfn73eyezg2wy094e3why592pghz27ruq3vkyegrv80eftnw9wqzwgvnwyseaus0yt84fylzrpzp6x2fguxuqjmgudr8xd33qm30evdpxd3jvjg8qh4q60kyq80jgff369k7nrepdc38grd2dava520excqp0ey0x39khx8ry03yffcatgv84fsx5j49djpapedsy693zute5xv5g2ewzrlj5se7akvkc4g4vmzhputpq8eyj9wz5dz6qtn7g3cfpd95nahw4ytspan0feyye04dcylv24ege7zkaj004gjwcxqxfqu2quawa83sx452jqjn8t48czp0xspwgnmvjyhttzzy6nhq8xzkdwnvsfefkwva6asrqc93zjn4rly5gnlv93xy3uzmr39szvjnf63426qzyeyvguc4vdcquwgsxgq236afcpqz866ny4tn7ckc0umefj242rt5vtvwqzzrvfev2mpvqcufp9pqvefyv4ftyuhgausfzuaadsczeykmft5wv3frzgrcp9ztr93h478ke4t86spp2uhyjkj73mp9g92ddk2fpv7v3njzsqgwhq3789sqrgkskehn0zjscckhwftyq4vet7vrlx2hs5kd9cwnq6t0djffhh3zquh4j3p0yaj9z2rc9wykg0usqw7983rrgur9jg8rnnqypwcz2lyclnnc705fc5g3an93ps60q6mxqp85u0ewtxdjlqcks84yduft0a0g6e7naew3v9u2d08knarvajn8q3gq9pgxde3s7nx94lus48wwvw2xjm7k82tvylec2393jdsuvch2xpe77w8hpv9nvsxfsrs270njpmfvpmgyk2cffl9tjp3qqcc4dfkf5rme2dg0x7ew8g39www5smm705q5da4eqvnqwrkavtq6xje9ss38hnkglz4eddz8f5qruvqmq2ff9l22gwkv8h432rdkysy0grkul8e2fedvkyyapfxt760udcgu92m54wl9yavmj4ga3ph9r5n99cjrq6wj5v33x33fe5vkjvfwnnt40wuv2hyexc9f4ylyqv9ldqq9epd4yuv8vrsfx2qy2kqz08kqhnzspy6s0x8fa5c2xkg5y2q0rvz4vnk7rp0acg6eksc3t7cxnn8y7glkjsqja3p56uz6vvhcw55d3ysad0hvsqxpjnc7svenf2gc5xn5kyr0et2vvyruxlnpqcdpqh9pzplumy5yzjxftyzh9ujfw0jq7ee60zx2x23p0jzyh9dvmly8p9h9ysptlqu7kwnejd65dnr75a0np2fvke8xen38r57w6z3wz3mycjmmn267wwxndfh9jdps7uxtct2wwfgamkpa5ap8s96lhfjztpwcm6fguhphu38yunu2v4vz3syzrvgwtqpemkewzp766nyu6texxvjlaemnhyyqutkcy6a42vqfsz49rw5wr4gt70r4vdaasehqjg46fnyts4sthrxadfllha3avu49wsj2c4jx
The hybrid recipient is secure against future cryptographically-relevant quantum computers, so the same file SHOULD NOT be encrypted to both a hybrid recipient and to other non-quantum-resistant recipient types.
The recipient encodings can be interpreted as plugin recipients with names tag
or tagpq, allowing for backwards compatibility with existing clients through
plugins.
To produce a p256tag recipient stanza, the file key is encrypted with the HPKE SealBase function from RFC 9180, Section 6.1, with the following parameters:
- KEM: DHKEM(P-256, HKDF-SHA256)
- KDF: HKDF-SHA256
- AEAD: ChaCha20Poly1305
pkR = decompress(recipient)(the uncompressed P-256 point)info = "age-encryption.org/p256tag"aad = ""(empty)
The recipient MUST be converted from its compressed form to uncompressed form before being used with HPKE's DeserializePublicKey function.
The result is then encoded as a recipient stanza with three arguments: the first
is the fixed string p256tag, the second is the base64-encoded tag, and the
third is the base64-encoded encapsulated key enc from SealBase.
tag = HKDF-Extract-SHA-256(ikm = enc || SHA-256(recipient)[:4], salt = "age-encryption.org/p256tag")[:4]
The recipient is included in the tag as a truncated SHA-256 hash of the compressed P-256 point encoding for backwards compatibility with existing age-plugin-yubikey identities, which used that truncated hash as the tag.
The body of the recipient stanza is the HPKE ciphertext from SealBase.
-> p256tag CXBuUw BOqTylUtZFwkMTxd6UENg8kopiMhRJBOUXMv5w2xVOmJu7eezoALGXiNtq5Vka+UQvaYbQvAdwjIJEDaoBaq7So
bZQpLsV8uGqGPs70J6dVodiHejsZ5BODjaRsB0RcBqI
The identity implementation MUST ignore any stanza that does not have p256tag
as the first argument, and MUST otherwise reject any stanza that has more or
less than three arguments, or where the second argument is not a canonical
base64 encoding of a 4-byte value or the third argument is not a canonical
base64 encoding of a 65-byte value. It MUST check that the body length is exactly
32 bytes before attempting to decrypt it, to mitigate partitioning oracle attacks.
To produce a mlkem768p256tag recipient stanza, the file key is encrypted with the HPKE SealBase function from RFC 9180, Section 6.1 with the following parameters:
- KEM: MLKEM768-P256 from draft-ietf-hpke-pq-03/filippo.io/hpke-pq
- KDF: HKDF-SHA256
- AEAD: ChaCha20Poly1305
pkR = recipientinfo = "age-encryption.org/mlkem768p256tag"aad = ""(empty)
The result is then encoded as a recipient stanza with three arguments: the first
is the fixed string mlkem768p256tag, the second is the base64-encoded tag, and
the third is the base64-encoded encapsulated key enc from SealBase.
tag = HKDF-Extract-SHA-256(ikm = enc || SHA-256(recipient[1184:])[:4], salt = "age-encryption.org/mlkem768p256tag")[:4]
The recipient is included in the tag as a truncated SHA-256 hash of the uncompressed P-256 point encoding only, because the ML-KEM encapsulation key might not be available without user presence (depending on how it is stored on the hardware), and to reduce the size of the identity encoding (which may choose to store only the truncated hash).
The body of the recipient stanza is the HPKE ciphertext from SealBase.
-> mlkem768p256tag Ke5Xmw xCwuu/DaqxpCEA6pOJO1rLO4BG+BCgL7jG3r50doA/Ns/HWS6zvgvDWPX+4qCBgwldrp3OwYn5baW9CTkDqgWTCcTUDdfZW9v/CP+Hw68XNUOVRLd3gJrIXSSJlw+pW87xQLJrn0QRykKmBgaBoV9JHV0w0mkcCq6Hyv5gvP6ZG/3Xd384pTVTl67Qj9YhBTn0haURN/F0QkBol0PmcDYje14+UU6lM/juXPs/gdZqKCyWVunlvDSKCyHwGL0sJP1oqNtwhyTkVh04x66+2u0yNxc0byfbx522AZZJdSGP1RfR855/tyNJLo2C5k1meSL78vBkuZk9eUEvyoPbK3KIl7D9dcZR93ECoHPeDBUQKNo6uzvAC8OmpU9NsJ3i/9Is36+0MbB5AZtFdSb4L6gw/ecPhK4vl85nRL5Yq1BkWXlrYsXltoStsbMBvV/xOPwsrMrUqGATrR5zypN9wVsi4xrtQYp5Qy2a7ZewzgiktO+lWGWR5n3ik4rN3/SgkeFE1/QRJFns81oV/Vqm6OaCzS3FM4jjjcAgqqJG+EGKCbOEG28xIbRuAyRX+TR/7JGlPRqgdMMTNv5LoB//hY6pUQq/zRFh2UgmtM7dWcuSB1BJ+2tH+S7IssErO2l/1Yj2363QuKozVLpHwL2mt3vV8j3aefWHIZPlX+kxdC/YEGRkRjN23eeozQFvLbVJjU4GWoJb++q8HYRNxLLRwfvOACzKj8I61OIfhzK9ho58lgtENYD43n+C+Z16t4nG3stUBvzgWKohOLJXL/AxOgghvrpkgzOYZl1E1LTWqWRqz//0bESfCziZlKrwJw117/orYjDCBpAtgHSdBiLU724oumOSq+A8TwBjHSTkZc2L3CNul1ENhxxOeKj5KUKy0bMgIJSIdUtBfgGJPLzsrMxyw/3B2H2Jx7F0zY0crWMc/1ok7MxFT+a7UJguRK15lHmWoqKZPh1KEJ2L/BkMSYh2IWlQAg1OAq4OfhaIL3RTHHkihhciY4B9U9iok+dfco/cmFALEzPjkVSNPT0Q0st5y5G++zz/K4/KWjfYqdSnTawnecASDXcQB+AcVc88fyQpfAHC3i2JNSyJVoGhEy8gdiJzshS6enDl+4WDaIOXS/oEQB8X4uIhY9VkRn381kS5KXBgsRvDF1HCNYuTDbYFjmcbcxmFVRX0gmNj7voazh1rgtDAjg3KwQBDDe/mltP2eGmas9lPo6PyV3F/N6iDpkCoQVRgw/mjWTsiFR8aGDEd97Ot7TmhFGSQiKXfgJeip0kLZB2/dP9Z9ctKVWa8Twx58CN/UKFku27ShdW6wilUsXdcrcrrDr9BWTP1k+N6rEOVqmzfjfW7naKUPMu1cS7yCqu5Qyzkmpvq7aDB+eHR8VGzqEj9YK0HNHr+rGDcULiHT+DO7eyf8WOjHa3AaGtxq46l01/15Xh0FPM78E8JXpz8oBxelHzQSfQDfHZhQDdNdD/cB52lfs0S8xxlPrLLpA0aWImDOctYhCHB6/A1Hi5XDdDyh/NVzTGbpSOg
PFa8qHdf8j5x6tJe7BoPCU0KGcSz1rgJO1my2ZPsJT0
The identity implementation MUST ignore any stanza that does not have
mlkem768p256tag as the first argument, and MUST otherwise reject any stanza
that has more or less than three arguments, or where the second argument is not
a canonical base64 encoding of a 4-byte value or the third argument is not a
canonical base64 encoding of a 1153-byte value. It MUST check that the body length is
exactly 32 bytes before attempting to decrypt it, to mitigate partitioning oracle
attacks.
age files that need to be transmitted as 7-bit ASCII SHOULD be encoded according
to the strict PEM encoding specified in RFC 7468, Section 3 (Figure 3), with
case-sensitive label "AGE ENCRYPTED FILE". Note that this encoding employs
base64 with = padding characters, unlike the rest of this document.
Note that ASCII armored files are malleable unless care is taken to reject any data before and after the PEM encoding, a strict PEM parser is used, and canonical base64 is enforced. age implementations SHOULD reject non-canonical ASCII armor encodings except for whitespace before and after the PEM block, and MAY choose to accept both LF and CRLF line endings.
A comprehensive set of test vectors is avaliable at https://age-encryption.org/testkit.