Twoway
Encrypted request-response messaging using HPKE in go
Install / Use
/learn @openpcc/TwowayREADME
twoway: Encrypted request-response messaging using HPKE.
twoway is a Go package that provides encrypted request-response messaging using HPKE.
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.Readerinterface. - 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
NewMultiRequestSenderandNewMultiRequestReceiver. - Create a request sealer as normal.
- Call
EncapsulateKeyon 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.
Related Skills
node-connect
345.4kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
104.6kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
345.4kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
345.4kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
