Hybrid Public Key Encryption (HPKE)
HPKE (RFC 9180) is a cryptographic protocol that enables secure communication using a combination of public key (ECC Diffie-Hellman) and symmetric encryption (AEAD) techniques.
It allows a sender that knows the public key of the intended recipient, to encrypt one or more messages ensuring confidentiality and integrity but without requiring an online key exchange.
There are no significant limits on the size of one message or on the number of messages in the sequence. Along with each encrypted message (ciphertext), the sender can also deliver a piece of non-sensitive data (e.g., a header), which will be authenticated together with the matching message (authenticated associated data, AAD).
Before, or together with the first message, the sender must deliver the encapsulated session secret (enc) to the receiver.

In the basic scheme above, the receiver does not receive any proof of identity from the sender. Variants of the protocol exist to enable the receiver to ascertain the origin of the message.
First (Auth mode), the sender can contribute its own private key to the encryption. The receiver must have the matching public key:

Second (PSK mode), the sender and the receiver can agree on the same secret key (at least 32 random bytes), and contribute that to both encryption and decryption.

Finally, the two authentication modes can also be combined together (AuthPSK mode).
Note
The PSK is only used to authenticate the messages; it is not a replacement for the asymmetric key that the receiver must provide to the sender.
Examples
This is how the sender can encrypt two messages:
from Crypto.Protocol import HPKE
# Let's assume that the recipient delivered its public key
# which is already loaded in a variable called their_pub_key
#
# For the sake of clarity, it could be simulated with:
#
# from Crypto.PublicKey import ECC
# their_key = ECC.generate(curve='p256')
# their_pub_key = their_key.public_key()
encryptor = HPKE.new(receiver_key=their_pub_key,
aead_id=HPKE.AEAD.AES128_GCM)
ct_1 = encryptor.seal(b'Message 1')
ct_2 = encryptor.seal(b'Message 2')
# The sender will deliver:
# - encryptor.enc as enc
# - ct_1
# - ct_2
And this is how the receiver can decrypt them:
from Crypto.Protocol import HPKE
# Let's assume that the recipient has its own private key
# which is already loaded in a variable called my_key
#
# To continue the simulation, assign:
#
# my_key = their_key
decryptor = HPKE.new(receiver_key=my_key,
aead_id=HPKE.AEAD.AES128_GCM,
enc=enc)
# Any of the calls to unseal() can raise ValueError
# if a key mismatch, modification to a message, or reordering is detected
pt1 = decryptor.unseal(ct_1)
pt2 = decryptor.unseal(ct_2)
Specification
- class Crypto.Protocol.HPKE.AEAD(value)
Authenticated Encryption with Associated Data (AEAD) Functions
- AES128_GCM = 1
- AES256_GCM = 2
- CHACHA20_POLY1305 = 3
- Crypto.Protocol.HPKE.new(*, receiver_key, aead_id, enc=None, sender_key=None, psk=None, info=None)
Create an HPKE context which can be used:
by the sender to seal (encrypt) a message or
by the receiver to unseal (decrypt) it.
As a minimum, the two parties agree on the receiver’s asymmetric key (of which the sender will only know the public half).
Additionally, for authentication purposes, they may also agree on:
the sender’s asymmetric key (of which the receiver will only know the public half)
a shared secret (e.g., a symmetric key derived from a password)
- Parameters:
receiver_key (
EccKey
) –The ECC key of the receiver. It must be on one of the following curves:
NIST P-256
,NIST P-384
,NIST P-521
,X25519
orX448
.If this is a public key, the HPKE context can only be used to seal (encrypt).
If this is a private key, the HPKE context can only be used to unseal (decrypt).
aead_id (
AEAD
) –The HPKE identifier of the symmetric cipher. The possible values are:
HPKE.AEAD.AES128_GCM
HPKE.AEAD.AES256_GCM
HPKE.AEAD.CHACHA20_POLY1305
enc (
Optional
[bytes
]) –The encapsulated session key (i.e., the KEM shared secret).
The receiver must always specify this parameter.
The sender must always omit this parameter.
sender_key (
Optional
[EccKey
]) – The ECC key of the sender. It must be on the same curve as thereceiver_key
. If thereceiver_key
is a public key,sender_key
must be a private key, and vice versa.psk (
Optional
[tuple
[bytes
,bytes
]]) –A Pre-Shared Key (PSK) as a 2-tuple of non-empty byte strings: the identifier and the actual secret value. Sender and receiver must use the same PSK (or none).
The secret value must be at least 32 bytes long, but it must not be a low-entropy password (use a KDF like PBKDF2 or scrypt to derive a secret from a password).
info (
Optional
[bytes
]) – A non-secret parameter that contributes to the generation of all session keys. Sender and receive must use the same info parameter (or none).
- Return type:
- Returns:
An object that can be used for sealing (if
receiver_key
is a public key) or unsealing (ifreceiver_key
is a private key). In the latter case, correctness of all the keys and parameters will only be assessed with the first call tounseal()
.
- class Crypto.Protocol.HPKE.HPKE_Cipher(receiver_key, enc, sender_key, psk_pair, info, aead_id, mode)
-
enc:
bytes
The encapsulated session key.
- seal(plaintext, auth_data=None)
Encrypt and authenticate a message.
This method can be invoked multiple times to seal an ordered sequence of messages.
- Parameters:
plaintext (
bytes
) – bytes The message to seal.auth_data (
Optional
[bytes
]) – bytes Optional. Additional Authenticated data (AAD) that is not encrypted but that will be also covered by the authentication tag.
- Returns:
The ciphertext concatenated with the authentication tag.
- unseal(ciphertext, auth_data=None)
Decrypt a message and validate its authenticity.
This method can be invoked multiple times to unseal an ordered sequence of messages.
- Parameters:
cipertext – bytes The message to unseal.
auth_data (
Optional
[bytes
]) – bytes Optional. Additional Authenticated data (AAD) that was also covered by the authentication tag.
- Returns:
The original plaintext.
- Raises: ValueError
If the ciphertext (in combination with the AAD) is not valid.
But if it is the first time you call
unseal()
this exception may also mean that any of the parameters or keys used to establish the session is wrong or that one is missing.
-
enc: