I did consider magic numbers for the types, however, the current version of Age is extensible without active coordination - that is, if you're designing a custom recipient type you call it "example.com/whatever", and you can be pretty sure it's globally unique, whereas with a small magic number you might collide with someone else's extension.
I didn't use a overall header length for two reasons:
- Having length fields that potentially "overlap" introduces parsing edge-cases. For example, what happens if the final recipient "overflows" the length specified earlier? Obviously you'd add bounds checks for this, but another implementer might forget!
- An implementation should scan over all the recipient fields to check that the file is well-formed before it attempts decryption. Not including the length forces parsers to do this. I don't think there's meaningful overall performance to be gained by skipping ahead anyway (since time spent doing X25519 or scrypt will dwarf the time it takes to do the parsing).
- Reason 2b - it should be possible to parse the file in a fully streaming fashion, with bounded memory usage (no dynamic allocations), without needing any hard limits. I might do a C implementation that demonstrates this concept.
> Having length fields that potentially "overlap" introduces parsing edge-cases.
Oh, that's a very good point actually. I'll keep that in mind, thanks.
> An implementation should scan over all the recipient fields to check that the file is well-formed before it attempts decryption.
Ideally an implementation should authenticate the whole header.
This means appending an authentication tag to the header, computed with from the file key. Of the top of my head, I would derive a header key and a payload key from the file key, using either a stream cipher, a hash, HMAC, or HKDF expand. Then I'd use the header key to authenticate the header. Probably using a keyed hash or HMAC to get key committent and avoid partition attacks down the line. Or, if key commitment is handled in the stanza themselves, with a fast polynomial hash.
> I might do a C implementation
In my opinion file formats should have a C implementation whenever possible: if a language as weak and as unsafe as C can handle it without too much trouble, we know it's a simple enough format.
> Then I'd use the header key to authenticate the header. Probably using a keyed hash or HMAC to get key committent
This is already the case :)
> In my opinion file formats should have a C implementation whenever possible
Yup, I totally agree. Although I haven't written any of the code yet, I've been architecting a hypothetical implementation in my head while designing the format.
I didn't use a overall header length for two reasons:
- Having length fields that potentially "overlap" introduces parsing edge-cases. For example, what happens if the final recipient "overflows" the length specified earlier? Obviously you'd add bounds checks for this, but another implementer might forget!
- An implementation should scan over all the recipient fields to check that the file is well-formed before it attempts decryption. Not including the length forces parsers to do this. I don't think there's meaningful overall performance to be gained by skipping ahead anyway (since time spent doing X25519 or scrypt will dwarf the time it takes to do the parsing).
- Reason 2b - it should be possible to parse the file in a fully streaming fashion, with bounded memory usage (no dynamic allocations), without needing any hard limits. I might do a C implementation that demonstrates this concept.