Class EncryptedCodec

java.lang.Object
org.acemq.amqp.crypto.EncryptedCodec
All Implemented Interfaces:
Codec

public final class EncryptedCodec extends Object implements Codec
Encrypts whatever another codec produced, so the broker holds ciphertext.

 Codec encrypted = EncryptedCodec.wrapping(new JsonCodec(), keyring);

 mq.publisher("payments", "card.stored", Card.class).as(encrypted);
 mq.consume("payments.stored", Card.class, encrypted, m -> store(m.payload()));
 

It wraps a delegate rather than serialising anything itself, so choosing a format and choosing to encrypt stay independent: JSON in, AES-GCM out, and Avro just as well.

What is on the wire

 0xAE  0x01  len  key identifier   12-byte nonce   ciphertext + 16-byte tag
 

The key identifier is in the message, in the clear. That is deliberate, and it is what makes rotation possible: a consumer reads which key a message needs rather than assuming the current one, so a new key can be introduced while messages written with the old one are still queued. Putting it in an AMQP header instead would have been tidier and would have lost it — headers are dropped by shovels, rewritten by federation, and absent from a message recovered out of a backup, and a ciphertext whose key nobody can name is gone.

The header is authenticated but not encrypted: GCM binds it as associated data, so an altered key identifier fails to decrypt rather than quietly decrypting as something else.

What this does not do

The broker can no longer read the message, and neither can the people who operate it. Decide what they do instead before turning this on: a dead-letter queue full of ciphertext is a queue nobody can triage, and the answer is usually a small internal tool holding the keyring rather than the management UI. See keyIdOf(byte[]), which tells an operator which key a message needs without holding any of them.

Encryption is not authorisation. Every service holding the keyring can read every message encrypted with those keys; the granularity is the key, so separate audiences mean separate keys.

Nor does it hide the routing. Exchange, routing key, headers and message size stay in the clear, and for many systems the routing key is the sensitive part.

  • Field Details

    • CONTENT_TYPE

      public static final String CONTENT_TYPE
      Deliberately not ...+json, whatever the plaintext underneath is.

      A +json suffix is a promise that the bytes on the wire are JSON, and every JSON-aware consumer reads it that way. These bytes are ciphertext. Naming them +json makes the JSON codec volunteer to decode them, which is how a message ends up failing in a parser rather than being refused by a codec that knows it cannot help.

      See Also:
  • Method Details

    • wrapping

      public static EncryptedCodec wrapping(Codec delegate, Keyring keyring)
      Parameters:
      delegate - the codec that turns objects into bytes; those bytes are what gets encrypted
      keyring - the keys to write with and read with
      Returns:
      a codec that encrypts the delegate's output
    • contentType

      public String contentType()
      Specified by:
      contentType in interface Codec
      Returns:
      the content type this codec produces, for example application/json
    • encode

      public byte[] encode(Object payload)
      Specified by:
      encode in interface Codec
      Parameters:
      payload - object to encode; never null
      Returns:
      encoded bytes
    • decode

      public <T> T decode(byte[] body, Class<T> target)
      Specified by:
      decode in interface Codec
      Type Parameters:
      T - target type
      Parameters:
      body - encoded bytes
      target - type to produce
      Returns:
      the decoded payload
    • canDecode

      public boolean canDecode(@Nullable String contentType)
      Specified by:
      canDecode in interface Codec
      Parameters:
      contentType - content type of a received message, or null when the sender did not set one
      Returns:
      whether this codec can decode it
    • keyIdOf

      public static @Nullable String keyIdOf(byte[] body)
      Reads which key a message needs, without needing the key.

      For the operator looking at a dead-letter queue they can no longer read. The identifier is in the clear in front of the ciphertext, so this answers "which key does this need?" from the bytes alone — which is usually the question, because a queue full of undecryptable messages is normally a key that was retired too early rather than anything wrong with the messages.

      Parameters:
      body - a message body
      Returns:
      the key identifier, or null if this was not written by this codec
    • delegate

      public Codec delegate()
      Returns:
      the wrapped codec, whose output is what gets encrypted
    • toString

      public String toString()
      Overrides:
      toString in class Object