HORIZON HASKELLDocslts/ghc-10.0.x8174fa42026-10-11Search names, modules, packages, or :: a typeCtrl K

GHC 10.0.0.20260917 · lts/ghc-10.0.x · 8174fa4 · 2026-10-11

Modulecrypton-2.1.7Haskell2010

Crypto.Cipher.AES.GCM

AES-GCM for callers that send many short messages under one key, which is what a datagram transport does.

The interface in Crypto.Cipher.Types builds a state from the key and the nonce and then walks it through appending the additional data, encrypting and finalizing, copying the state at each step. For a stream that is nothing next to the encryption. For a QUIC packet it is most of the work: the key schedule and the table of multiples of H depend on the key alone, and rebuilding them for every nonce costs more than encrypting 1440 bytes.

So here a Context is built from the key once and holds both, and encrypt takes a nonce and a whole message and answers in one call.

ctx <- throwCryptoError <$> pure (newContext key)
let packet = encrypt ctx nonce header plaintext 16

This runs on AES-NI and carry-less multiply, or on the ARMv8 cryptographic extension, and makes no branch and no memory access that depends on the key or on the data. Where the processor has neither, AES falls back to a table driven implementation that is not constant time; see the side channels section of the README, and processorOptions for which is in use.

The result is the ciphertext with the tag after it, which is the shape a packet wants. decrypt takes that shape back, compares the tag itself and answers Nothing when it does not match.

This computes the same thing as the general interface; the tests hold it to that on the same vectors.

  • 2 types
  • 6 values
  • Packagecrypton-2.1.7
  • Exports8
  • LanguageHaskell2010
  • LicenceBSD-3-Clause AND MIT AND ISC
  • SourceGCM.hs
datadata Context
#

Everything a key determines: the AES key schedule and the table of multiples of H. Build it once and encrypt as many messages under it as the key is good for.

valueencrypt
  1. :: (ByteArrayAccess nonce, ByteArrayAccess aad, ByteArrayAccess ba, ByteArray output)
  2. => Context
  3. -> nonce
  4. -> aad
  5. -> ba
  6. -> Int
  7. -> output
#

Encrypt one message: the nonce, the additional data that is authenticated but not encrypted, the plaintext, and how many bytes of tag to produce, which GCM allows between 4 and 16. Any other length throws CryptoError_AuthenticationTagSizeInvalid, and so does decryptWithTag.

The answer is the ciphertext followed by the tag.

A nonce must not be used twice with the same Context. Twelve bytes is the size GCM is defined for and the only one that does not cost a further pass. A nonce of no bytes is refused: it would hand out the key GCM authenticates with, so encrypt and decryptWithTag throw CryptoError_IvSizeInvalid for it, decrypt gives Nothing and encryptWithMask gives False.

valuedecrypt
  1. :: (ByteArrayAccess nonce, ByteArrayAccess aad, ByteArray ba)
  2. => Context
  3. -> nonce
  4. -> aad
  5. -> ba
  6. -> Int
  7. -> Maybe ba
#

Decrypt one message, in the shape encrypt produced: the ciphertext with its tag after it. The tag is compared here, every byte of it whatever the answer, and a message whose tag does not match gives Nothing rather than the plaintext.

Nothing also comes back when the nonce has no bytes, the input is shorter than the tag, or the tag length is outside 4 to 16.

valuedecryptWithTag
  1. :: (ByteArrayAccess nonce, ByteArrayAccess aad, ByteArray ba)
  2. => Context
  3. -> nonce
  4. -> aad
  5. -> ba
  6. -> Int
  7. -> (ba, AuthTag)
#

Decrypt one message, the tag kept apart, and hand back the tag this end computed.

For a caller whose protocol hands it the tag separately from the ciphertext, so that decrypt -- which wants the two together and compares them itself -- does not fit. Compare the two tags with ==; the Eq instance of AuthTag is a constant-time comparison, and taking them apart to compare the bytes is how this goes wrong.

Nothing here says whether the message is authentic. Until the comparison is made and has come out equal, what this returns is not plaintext, it is what the ciphertext turns into, and a caller must not act on it.

Header protection

3 declarations
newtypenewtype HeaderKey
#

The key schedule for header protection, which QUIC keeps separately from the one it encrypts with. Built once, like a Context.

valueencryptWithMask
  1. :: (ByteArrayAccess nonce, ByteArrayAccess aad, ByteArrayAccess ba)
  2. => Context
  3. -> HeaderKey
  4. -> nonce
  5. -> aad
  6. -> ba
  7. -> Int

    tag length

  8. -> Int

    sample offset

  9. -> Ptr Word8

    where the sealed message goes

  10. -> Ptr Word8

    where the sixteen bytes of mask go

  11. -> IO Bool
#

Encrypt one message and, from a sample of the ciphertext it just produced, make the header protection mask -- in one call, into two buffers the caller already has.

QUIC takes its sample from the ciphertext, so the mask cannot be had before the encryption. It can be had before coming back, and with the buffers already there nothing is allocated for either. On an Apple M4 the mask then costs about 0.02 us, where asking for it separately costs 0.11.

The sealed message wants length input + taglen bytes and the mask sixteen. sampleOffset says where the sixteen bytes of sample begin in the sealed message, counting the tag as part of it.

False comes back, and nothing is written, when the nonce has no bytes, the sample would not fit, or the tag length is outside 4 to 16.