twoway

package module
v0.0.80 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 11, 2025 License: Apache-2.0 Imports: 10 Imported by: 0

README

twoway: Encrypted request-response messaging using HPKE.

twoway is a Go package that provides encrypted request-response messaging using HPKE.

Go Reference

Overview

twoway allows a sender to send a request message to one (or more) receivers, and for those receiver(s) to send back a response message. Twoway then guarantees the integrity of this roundtrip by cryptographically tying the response message to the request message.

HPKE sealed messages always flow in one direction: sender->receiver. HPKE guarantees that only the intended receiver can decrypt the message.

twoway adds a return leg to this flow. It models a flow in two directions, sender->receiver->sender if you will. twoway guarantees that:

  • The request message can only be decrypted by the intended receiver.
  • The response message can only have been sent by the intended receiver.
  • The response message was in response to the request message.

Features

  • One-to-one and one-to-many messaging.
  • Chunked and non-chunked messages, both using the io.Reader interface.
  • One-to-one messaging is fully compatible with the OHTTP and Chunked OHTTP.
  • For power users: Allows for injection of custom HPKE components to support hardware integration.
  • Build on top of the primitives provided by cloudflare/circl.

Walkthrough

In this example a sender sends a regular request to a receiver. Let's assume we have a hpke.Suite and keys set up.

First, we need to create a sender. In the context of HTTP apps, these will often be created on the client.

// the sender sends a regular request
sender, err := twoway.NewRequestSender(suite, keyID, receiverPubKey, rand.Reader)
if err != nil {
	// handle error
}

This sender then creates a request sealer to seal our secret message.

This request sealer also needs a media type, this media type needs to match when decrypting the request. Baking the media type into the encrypted message makes it a lot less likely that someone can trick the receiver into interpreting this message in the wrong way.

You're free to choose any media type you want.

reqSealer, err := sender.NewRequestSealer(bytes.NewReader("a secret message"), []byte("secret-req"))
if err != nil {
	// handle error.
}

The reqSealer is an io.Reader, you can read from it to get your encrypted message.

reqCiphertext, err := io.ReadAll(reqSealer)
if err != nil {
	// handle error
}

A receiver is created as follows. Again, when dealing with HTTP apps these will often be created on the server.

reqReceiver, err := twoway.NewRequestReceiver(suite, keyID, receiverPrivateKey, rand.Reader)
if err != nil {
	// handle error.
}

This receiver can now create an opener to open our earlier reqCiphertext. The media type needs to match our earlier media type.

reqOpener, err := reqReceiver.NewRequestOpener(bytes.NewReader(reqCiphertext), []byte("secret-req"))
if err != nil {
	// handle error
}

Again, the reqOpener is an io.Reader so we can read from it to get the plaintext.

reqPlaintext, err := io.ReadAll(reqOpener)
if err != nil {
	// handle error
}

// reqPlaintext now contains []byte("a secret message")

With the request handled, let's write back a response in chunks.

A chunked response.

Let's say we have an io.Reader called source that reads data from some kind of stream.

The reqOpener allows you to create a response sealer, but by default it will write a non-chunked response. We need to enable chunking by providing it with the twoway.EnableChunking option.

respSealer, err := reqOpener.NewResponseSealer(
	source, []byte("secret-chunked-resp"), twoway.EnableChunking(),
)
if err != nil {
	// handle error
}

You can now read ciphertext chunks from respSealer.

Back on the sending side, we can pass these chunks (or this reader directly) to a response opener. This can be created via reqSealer we created earlier. We again need to match the media type, but also need to enable chunking.

respOpener, err := reqSealer.NewResponseOpener(
	respSealer, []byte("secret-chunked-resp")), twoway.EnableChunking(),
)
if err != nil {
	// handle error
}

By reading from the respOpener you will now get the plaintext response in chunks.

One-to-many messaging

One-to-many messaging works similar to one-to-one messaging.

The differences are as follows:

  • Create sender and receiver using NewMultiRequestSender and NewMultiRequestReceiver.
  • Create a request sealer as normal.
  • Call EncapsulateKey on the request sealer for each receiver.
  • Provide the resulting encapsulated key to each receiver together with the ciphertext.
  • The response flow is the same as in one-to-one messaging.

Found a security issue?

Reach out to security@confidentsecurity.com.

Thread Safety

The package makes no guarantees about thread safety. Concurrent access should be externally synchronized.

Development

Run tests with go test ./...

Other Work

cloudflare/circl, and tink both provide HPKE implementations in go but neither support streaming bidirectional messages.

Documentation

Index

Constants

View Source
const BinaryRequestHeaderLen = 1 + 2 + 2 + 2

BinaryRequestHeaderLen is the binary encoded length of an OHTTP Request Header.

Variables

This section is empty.

Functions

This section is empty.

Types

type HPKEExporter

type HPKEExporter interface {
	Export(exporterContext []byte, length uint) []byte
}

HPKEExporter is a HPKE exporter. Defined as an interface to allow the use of non-circl HPKE implementations in this package.

type HPKEOpener

type HPKEOpener interface {
	HPKEExporter
	Open(ct, aad []byte) ([]byte, error)
}

HPKEOpener is a HPKE opener. Defined as an interface to allow the use of non-circl HPKE implementations in this package.

type HPKEReceiver

type HPKEReceiver interface {
	Setup(enc []byte) (HPKEOpener, error)
}

HPKEReceiver is a HPKE receiver. Defined as an interface to allow the use of non-circl HPKE implementations in this package.

type HPKESealer

type HPKESealer interface {
	HPKEExporter
	Seal(pt, aad []byte) ([]byte, error)
}

HPKESealer is a HPKE sealer. Defined as an interface to allow the use of non-circl HPKE implementations in this package.

type HPKESender

type HPKESender interface {
	Setup(rnd io.Reader) ([]byte, HPKESealer, error)
}

HPKESender is a HPKE sender. Defined as an interface to allow the use of non-circl HPKE implementations in this package.

type HPKESuite

type HPKESuite interface {
	NewSender(pubKey kem.PublicKey, info []byte) (HPKESender, error)
	NewReceiver(privKey kem.PrivateKey, info []byte) (HPKEReceiver, error)
	Params() (hpke.KEM, hpke.KDF, hpke.AEAD)
}

HPKESuite is a HPKE suite. Defined as an interface to allow the use of non-circl HPKE implementations in this package.

func AdaptCirclHPKESuite

func AdaptCirclHPKESuite(suite hpke.Suite) HPKESuite

AdaptCirclHPKESuite adapts a Cloudflare Circl HPKE suite to a HPKE Suite that can be used with this package.

type MultiRequestReceiver

type MultiRequestReceiver struct {
	// contains filtered or unexported fields
}

MultiRequestReceiver receives request messages from MultiRequestSender.

When a ciphertext is received, it should be passed to MultiRequestReceiver.NewRequestOpener to create an opener.

This type and its opener are not compatible with OHTTP.

func NewMultiRequestReceiver

func NewMultiRequestReceiver(suite hpke.Suite, keyID byte, privKey kem.PrivateKey, randReader io.Reader) (*MultiRequestReceiver, error)

NewMultiRequestReceiver creates a receiver for the given HPKE suite and private key.

func NewMultiRequestReceiverWithCustomSuite

func NewMultiRequestReceiverWithCustomSuite(suite HPKESuite, keyID byte, privKey kem.PrivateKey, randReader io.Reader) (*MultiRequestReceiver, error)

NewMultiRequestReceiverWithCustomSuite allows for the use of a non-circl HPKE suite.

func (*MultiRequestReceiver) NewRequestOpener

func (r *MultiRequestReceiver) NewRequestOpener(encapKey []byte, ct io.Reader, mediaType []byte, opts ...Option) (*RequestOpener, error)

NewRequestOpener creates a new opener and begins opening the plaintext message.

The mediaType is used as additional context and must match the mediaType that was used to seal the ciphertext.

Enable chunking by passing the EnableChunking option.

type MultiRequestSealer

type MultiRequestSealer struct {
	// contains filtered or unexported fields
}

MultiRequestSealer seals a request message for multiple receivers.

MultiRequestSealer implements io.Reader, Read returns the ciphertext.

If chunking is enabled, the ciphertext will consist of one or more chunks. These chunks can be processed incrementally.

func (*MultiRequestSealer) EncapsulateKey

func (s *MultiRequestSealer) EncapsulateKey(keyID byte, pubKey kem.PublicKey) ([]byte, ResponseOpenerFunc, error)

EncapsulateKey encapsulates the internal secret key for a given public key of a receiver. The returned ResponseOpenerFunc should be used to create a response opener for this receiver.

func (*MultiRequestSealer) HeaderLen

func (s *MultiRequestSealer) HeaderLen() int

HeaderLen returns the length of the header of this message. Each sealer prefixes a message or a stream of chunks with a single header.

func (*MultiRequestSealer) Len

func (s *MultiRequestSealer) Len() (int, bool)

Len returns the remaining number of bytes that can be read.

Only applies sealers sealing unchunked messages. If this sealer is sealing a chunked message the second return value will be false.

Len includes the header length.

func (*MultiRequestSealer) MaxCiphertextChunkLen

func (s *MultiRequestSealer) MaxCiphertextChunkLen() (int, bool)

MaxCiphertextChunkLen returns the maximum length of a ciphertext chunk. If this sealerReader is not sealing a chunked message the second return value will be false.

The actual length of a ciphertext chunk depends on the length returned by original plaintext reader.

The returned length does not include the header length.

func (*MultiRequestSealer) Read

func (s *MultiRequestSealer) Read(p []byte) (int, error)

Read the ciphertext.

type MultiRequestSender

type MultiRequestSender struct {
	// contains filtered or unexported fields
}

MultiRequestSender sends request messages to multiple receivers. Messages can be received by MultiRequestReceiver.

Create a new sealer using MultiRequestSender.NewRequestSealer to begin sealing a plaintext.

After creating a sealer, the sealer's internal secret key can be encapsulated for one or more receivers. It up to the caller to provide this encapsulated key to the appropriate receiver.

This type and its sealer are not compatible with OHTTP.

func NewMultiRequestSender

func NewMultiRequestSender(suite hpke.Suite, randReader io.Reader) *MultiRequestSender

NewMultiRequestSender creates a new sender for the given HPKE suite.

func NewMultiRequestSenderWithCustomSuite

func NewMultiRequestSenderWithCustomSuite(suite HPKESuite, randReader io.Reader) *MultiRequestSender

NewMultiRequestSenderWithCustomSuite allows for the use of a non-circl HPKE suite.

func (*MultiRequestSender) NewRequestSealer

func (s *MultiRequestSender) NewRequestSealer(pt io.Reader, mediaType []byte, opts ...Option) (*MultiRequestSealer, error)

NewRequestSealer generates a new random key and begins sealing the plaintext message. The mediaType is used as additional context and must be matched when opening the ciphertext returned by this sealer.

Enable chunking by passing the EnableChunking option.

type Option

type Option func(*opConfig) error

Option is an option for a sealer or opener.

func EnableChunking

func EnableChunking() Option

EnableChunking splits the message into chunks that can be incrementely delivered. Use WithMaxChunkPlaintextLen to change the length of the chunks.

func WithInitialChunkBufferLen

func WithInitialChunkBufferLen(initialBufferLen int) Option

WithInitialChunkBufferLen provides a custom initial buffer length for chunked opening operations.

The provided length is a plaintext length, the actual buffer will be slightly larger to fit AEAD overhead.

This option is only supported when using openers.

func WithMaxChunkPlaintextLen

func WithMaxChunkPlaintextLen(maxChunkContentLen int) Option

WithMaxChunkPlaintextLen specifies the maximum length of plaintexts chunks. The maximum length of these chunks will be slightly larger to fit a chunk header and AEAD overhead.

Both sealers and openers accept this option.

Sealers will limit the chunks they generate to the maximum length. Openers will limit their receiving buffers to the maximum length.

The default values for sealers/openers are suitable for use with OHTTP. If you are using OHTTP keep in mind the Chunked OHTTP RFC:

https://www.ietf.org/archive/id/draft-ietf-ohai-chunked-ohttp-03.html#name-response-encapsulation.

Implementations MUST support receiving chunks that contain 2^14 (16384) octets of data prior
to encapsulation. Senders of chunks SHOULD limit their chunks to this size, unless they are
aware of support for larger sizes by the receiving party.

type RequestHeader

type RequestHeader struct {
	KeyID  byte
	KemID  hpke.KEM
	KDFID  hpke.KDF
	AEADID hpke.AEAD
}

RequestHeader is the OHTTP Header added to request messages.

func NewRequestHeaderForSuite

func NewRequestHeaderForSuite(suite HPKESuite, keyID byte) RequestHeader

NewRequestHeaderForSuite creates a new request header for the given suite and key ID.

func ParseRequestHeaderFrom

func ParseRequestHeaderFrom(ct []byte) (RequestHeader, error)

ParseRequestHeaderFrom parses the first 7 bytes of the ct as a Requestheader. ParseRequestHeaderFrom leaves ct untouched.

Use this function if you need to determine the suite and key id before instantiating a request receiver.

func (RequestHeader) MarshalBinary

func (h RequestHeader) MarshalBinary() ([]byte, error)

MarshalBinary marshals the RequestHeader in the OHTTP wire format.

func (*RequestHeader) UnmarshalBinary

func (h *RequestHeader) UnmarshalBinary(b []byte) error

UnmarshalBinary unmarshals the RequestHeader from the OHTTP wire format.

type RequestOpener

type RequestOpener struct {
	// contains filtered or unexported fields
}

RequestOpener opens a request message.

RequestOpener implements io.Reader, Read returns the plaintext.

func (*RequestOpener) NewResponseSealer

func (o *RequestOpener) NewResponseSealer(pt io.Reader, mediaType []byte, opts ...Option) (*ResponseSealer, error)

NewResponseSealer begins sealing the given plaintext. The mediaType is used as additional context and must be matched when opening the ciphertext returned by this sealer.

Enable chunking by passing the EnableChunking option.

func (*RequestOpener) Read

func (o *RequestOpener) Read(p []byte) (int, error)

type RequestReceiver

type RequestReceiver struct {
	// contains filtered or unexported fields
}

RequestReceiver receives request messages from RequestSender.

When a ciphertext is received, it should be passed to RequestReceiver.NewRequestOpener to create an opener.

This receiver accepts request formats as defined in the OHTTP RFC and Chunked OHTTP Draft RFC.

As Chunked OHTTP is still a draft, this functionality might be subject to change.

func NewRequestReceiver

func NewRequestReceiver(suite hpke.Suite, keyID byte, privKey kem.PrivateKey, randReader io.Reader) (*RequestReceiver, error)

NewRequestReceiver creates a new receiver for the given HPKE suite and private key.

func NewRequestReceiverWithCustomSuite

func NewRequestReceiverWithCustomSuite(suite HPKESuite, keyID byte, privKey kem.PrivateKey, randReader io.Reader) (*RequestReceiver, error)

NewRequestReceiverWithCustomSuite allows for the use of a non-circl HPKE suite.

func (*RequestReceiver) NewRequestOpener

func (r *RequestReceiver) NewRequestOpener(ct io.Reader, mediaType []byte, opts ...Option) (*RequestOpener, error)

NewRequestOpener creates a new opener and begins opening the plaintext message.

The mediaType is used as additional context and must match the mediaType that was used to seal the ciphertext.

Enable chunking by passing the EnableChunking option.

type RequestSealer

type RequestSealer struct {
	// contains filtered or unexported fields
}

RequestSealer seals a request message for a single receiver.

RequestSealer implements io.Reader, Read returns the ciphertext.

If chunking is enabled, the ciphertext will consist of one or more chunks. These chunks can be processed incrementally.

func (*RequestSealer) HeaderLen

func (s *RequestSealer) HeaderLen() int

HeaderLen returns the length of the header of this message. Each sealer prefixes a message or a stream of chunks with a single header.

func (*RequestSealer) Len

func (s *RequestSealer) Len() (int, bool)

Len returns the remaining number of bytes that can be read.

Only applies sealers sealing unchunked messages. If this sealer is sealing a chunked message the second return value will be false.

Len includes the header length.

func (*RequestSealer) MaxCiphertextChunkLen

func (s *RequestSealer) MaxCiphertextChunkLen() (int, bool)

MaxCiphertextChunkLen returns the maximum length of a ciphertext chunk. If this sealerReader is not sealing a chunked message the second return value will be false.

The actual length of a ciphertext chunk depends on the length returned by original plaintext reader.

The returned length does not include the header length.

func (*RequestSealer) NewResponseOpener

func (s *RequestSealer) NewResponseOpener(ct io.Reader, mediaType []byte, opts ...Option) (io.Reader, error)

NewResponseOpener creates a new opener and begins opening the plaintext message.

The mediaType is used as additional context and must match the mediaType that was used to seal the ciphertext.

Enable chunking by passing the EnableChunking option.

func (*RequestSealer) Read

func (s *RequestSealer) Read(p []byte) (int, error)

Read the ciphertext.

type RequestSender

type RequestSender struct {
	// contains filtered or unexported fields
}

RequestSender sends request messages to a single receiver. Messages can be received by RequestReceiver.

Create a new sealer using RequestSender.NewRequestSealer to begin sealing a plaintext.

The output of this sender and its sealers fully conforms to request formats defined in OHTTP RFC and Chunked OHTTP Draft RFC.

As Chunked OHTTP is still a draft, this functionality might be subject to change.

func NewRequestSender

func NewRequestSender(suite hpke.Suite, keyID byte, pubKey kem.PublicKey, randReader io.Reader) (*RequestSender, error)

NewRequestSender creates a new sender for the given HPKE suite and public key.

func NewRequestSenderWithCustomSuite

func NewRequestSenderWithCustomSuite(suite HPKESuite, keyID byte, pubKey kem.PublicKey, randReader io.Reader) (*RequestSender, error)

NewRequestSenderWithCustomSuite allows for the use of a non-circl HPKE suite.

func (*RequestSender) NewRequestSealer

func (s *RequestSender) NewRequestSealer(pt io.Reader, mediaType []byte, opts ...Option) (*RequestSealer, error)

NewRequestSealer creates a new sealer and begins sealing the plaintext message.

The mediaType is used as additional context and must be matched when opening the ciphertext returned by this sealer.

Enable chunking by passing the EnableChunking option.

type ResponseOpenerFunc

type ResponseOpenerFunc func(ct io.Reader, mediaType []byte, opts ...Option) (io.Reader, error)

ResponseOpenerFunc is a function that creates a response opener.

type ResponseSealer

type ResponseSealer struct {
	// contains filtered or unexported fields
}

ResponseSealer seals a response message.

ResponseSealer implements io.Reader, Read returns the ciphertext.

func (*ResponseSealer) HeaderLen

func (s *ResponseSealer) HeaderLen() int

HeaderLen returns the length of the header of this message. Each sealer prefixes a message or a stream of chunks with a single header.

func (*ResponseSealer) Len

func (s *ResponseSealer) Len() (int, bool)

Len returns the remaining number of bytes that can be read.

Only applies sealers sealing unchunked messages. If this sealer is sealing a chunked message the second return value will be false.

Len includes the header length.

func (*ResponseSealer) MaxCiphertextChunkLen

func (s *ResponseSealer) MaxCiphertextChunkLen() (int, bool)

MaxCiphertextChunkLen returns the maximum length of a ciphertext chunk. If this sealerReader is not sealing a chunked message the second return value will be false.

The actual length of a ciphertext chunk depends on the length returned by original plaintext reader.

The returned length does not include the header length.

func (*ResponseSealer) Read

func (s *ResponseSealer) Read(p []byte) (int, error)

Directories

Path Synopsis
internal
chunks
Package chunks encrypts/decrypts sequences of chunks using Authenticated Encryption with Associated Data.
Package chunks encrypts/decrypts sequences of chunks using Authenticated Encryption with Associated Data.
test/unsafehpke
package unsafehpke implements the Hybrid Public Key Encryption (HPKE) standard specified by draft-irtf-cfrg-hpke-07.
package unsafehpke implements the Hybrid Public Key Encryption (HPKE) standard specified by draft-irtf-cfrg-hpke-07.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL