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.