The various io.* interfaces (such as io.Reader and io.Writer) are often touted as a golang “killer feature”, though once you are familiar with interfaces you realise there is not much to them. The beauty is in the simplicity.

When I first looked at the encoding/gob I was slightly peturbed by the requirement to provide a io.Reader (to the Decoder) and an io.Writer (to the Encoder). It seemed like a strange way to go about things. Why couldn’t I just encode/decode a struct, get some bytes and do whatever I wanted with them?

(There are a few reasons for it in this particular case, however they aren’t the interesting part here).

I was looking at encoding/gob because I was writing netgiv and it seemed the easiest approach (compared to figuring out my own marshaling/unmarshaling of various structs).

Encryption was also mandatory for this project, and nacl seemed to be “the go” (pun intended) there.

The light bulb moment was when I realised that I just needed to implement the io.Reader and io.Writer methods for my encrypted connection, and I would then get the ability to use encoding/gob, over an encrypted connection for free.

type SecureConnection struct {
	Conn      io.ReadWriteCloser
	SharedKey *[32]byte
	Buffer    *bytes.Buffer

So - my SecureConnection contains a Conn which is an io.ReadWriteCloser. This is the actual transport - in this case a net.Conn TCP connection (client or server side). My implementations of Read() and Write() deal with the nacl encryption and decryption, transparently providing a cleartext stream to the gob encoder and decoder.

Then sending a packet over the wire, with encryption, is as simple as:

// create our connection and encoder
sc := SecureConnection{ ... }
enc := gob.NewEncoder(&sc)

// send some data
packet := SomeStruct{ ... }
err = enc.Encode(packet) // data is encoded and sent, encrypted

On the other side of the connection, we have something like:

// create our connection and decoder
sc := SecureConnection{ ... }
dec := gob.NewDecoder(&sc)

// read some data
packet := SomeStruct{}
err = dec.Decode(&packet) // data read from wire, decrypted and 
                          // decoded into the struct

In reality, both the client and the server would have both an encoder and a decoder, allowing bi-directional data flow.

Even better, since my Conn field on SecureConnection can be anything that implements io.ReadWriteCloser, I can do unit testing by creating an instance using net.Pipe so I can then test the encryption/decryption layer without needing to create a TCP connection to do so.

Tags: netgiv  encoding/gob  golang  encryption  nacl