If you squint at the example usage in the tests, it's basically the API that the blogpost describes.
https://github.com/peterldowns/symcrypt/blob/main/symcrypt_t...
As an aside, I'm always curious to understand why the encryption people say "never roll your own crypto" but then also ship confusing APIs without clear usage examples. For instance, check out the golang chacha20poly1305 docs:
Your `symcrypt` interface lands in a pretty weird place? AEADs in Go export "Seal" and "Unseal" --- with deliberately different names than crypto/cipher/Block's "Encrypt" and "Decrypt", because they're doing something different. The "Owner" thing in your package is kind of odd too.
You're exposing an interface over Go's AEAD primitives, but not letting users actually provide authenticated data. I don't much care except the whole point of this post is why that matters.
For me, personally, I'm going to side with tptacek - he has a track record that I have seen over at least a decade if not two.
I don't know the other bloke but this is a bit of a worry: "I'm not a cryptographer".
> Your `symcrypt` interface lands in a pretty weird place? AEADs in Go export "Seal" and "Unseal" --- with deliberately different names than crypto/cipher/Block's "Encrypt" and "Decrypt", because they're doing something different.
What should I use? I'd be extremely happy to do the Right Thing. I linked symcrypt and posted here because I am hoping someone can point me to it.
> You're exposing an interface over Go's AEAD primitives, but not letting users actually provide authenticated data.
I really don't understand what you mean by "not letting users actually provide authenticated data". Here, in this test, I show how if you encrypt some secret for one user (the associated data is the Owner), you can only decrypt it if you provide the same associated data (the same Owner). https://github.com/peterldowns/symcrypt/blob/c220f7767fa6c1a...
What you should do is just take the examples from cipher#AEAD, but where they do:
block, err := aes.NewCipher(key)
if err != nil {
panic(err.Error())
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
panic(err.Error())
}
Instead you just do chapoly, err := chacha20poly1305.NewX(key)
if err != nil {
panic(err.Error())
}
The rest of the code is the same, except that where they write "Never use more than 2^32 random nonces with a given key because of the risk of a repeat", you can ignore that and use a long nonce (like in the example for chacha20poly1305.NewX).Your "Owner" looks like what cryptographers would call a "domain separation constant". Domain separation is good! It's another application of authenticated data, too. But not the only one.
The Go standard library's AEAD "Seal" and "Unseal" is a better interface than what you've got now.
Another source of inspiration (and something I use in production) is the Tink family of cryptographic libraries by Google [1]. Their Go implementation [2] is not without its warts, but it's very difficult to run into any of those security bugs that exist around cryptography. Where the Go documentation lacks, there are some examples in the developer docs that help fill some of the gaps [3] [4].
The documentation isn't 100% complete, but I find it more discoverable than the standard library because while the standard library requires you to read both `crypto/cipher` and `crypto/aes` or `golang.org/x/crypto/chacha20poly1305` depending on what kind of cipher you want, Tink organizes it by use cases [5] and generally groups together all the things you need to do cryptographic operations under the use-case-named interfaces in the `tink` package [6], with the corresponding key generation templates located under the top-level packages of the same name [7].
[1]: https://developers.google.com/tink
[2]: https://github.com/tink-crypto/tink-go/
[3]: https://developers.google.com/tink/key-management-overview#g...
[4]: https://developers.google.com/tink/encrypt-data#go
[5]: https://developers.google.com/tink/choose-primitive
[6]: https://pkg.go.dev/github.com/tink-crypto/tink-go/v2/tink#AE...
[7]: https://pkg.go.dev/github.com/tink-crypto/tink-go/v2@v2.4.0/...
I wrote a local file encryption tool, around the same time Filippo was doing `age`, and used the AD on Chapoly to authenticate the chunk offset into the file. (The only thing interesting my tool did was that it could pull keys from AWS KMS).
So one use for AD is to authenticate headers; another is contextual binding.
If it helps (because 'stavros asked across the thread why bother having AD at all rather than just including it in the ciphertext), authenticated data can include data that doesn't even appear in the message, but rather is derived from the context at the time the message is encrypted and decrypted. A message only meant to be decrypted on a particular host (or whatever), for instance, could include the host in its AD, but never record that in the actual bits of the message.
https://news.ycombinator.com/item?id=43827342
Honestly, the classic "message routing" example most things give for AEAD is not very useful. Context binding is a much better primer for intuition.
Basically, I'm not sure why `encrypt(key, nonce, (data, associated data))` (ie adding the AD to your ciphertext, with the encryption framework being unaware of it) is that different from `encrypt(key, nonce, data, associated data)` (ie the AD being a first-class citizen).
EDIT: I saw your other message, and this makes it click for me:
> authenticated data can include data that doesn't even appear in the message, but rather is derived from the context at the time the message is encrypted and decrypted
So the AD can be an additional envelope-level thing at encryption/decryption time, that helps a lot, thanks!
Instead, just take the chunked large-file encryption use case I gave in that comment. The chunk offset isn't recorded anywhere in the ciphertext. It's derived contextually while you decrypt the file. The AD ensures that the decryption will explode if you try to cut and paste chunks of the file into different positions.
You could water down the example a bit to make it work:
1. Assume there's some other authentication mechanism for client-server communication, e.g. TLS.
2. The client sends the user ID unencrypted (within TLS) so the server can route, but encrypts the message contents so the server can't read it.
3. The final recipient can validate the message and the user ID.
This saves the client from having to send the user ID twice, once in the ciphertext and once in the clear.
But another more interesting use case is when you don't even send the associated data: https://news.ycombinator.com/item?id=43827342
just the right length and pacing to get me to the end and the point across
halosghost•2h ago
All the best,
-HG