Let’s walk through a Golang NaCL based crypto-box that can be unlocked with one of two keys, perfect for sharing data between two users.

Starting with the building blocks..

Background

What is NaCL?

NaCL (“salt”) is a library that provides many cryptographic building blocks. It implements state-of-the-art primitives for public-key and secret-key cryptography (as well as other things we won’t get into here).

Ports of these building blocks are available in Go via the crypto/nacl extensions.

Why should I care?

NaCL is important, because (presumably) you aren’t a cryptographer. Implementing these algorithms correctly is incredibly difficult and time consuming. By using a library like NaCL, you get to take advantage of the significant effort developers and researchers have put into validating the correctness of the code. We are in the fortunate position of not having to care about how Salsa20 works, or how to implement performance ed25519 signatures; we can just build on top of NaCL.

What’s the problem?

Let’s say Alice wants to send a secret message to Bob.

graph LR; Alice-->|Message|Bob

With standard public-key crypto (like NaCL’s box), Alice can encrypt the message using Bob’s public key and send that to Bob, knowing that only Bob will be able to open it. But what if Alice forgets what she sent? If there was a way she could also decrypt the ciphertext, she wouldn’t even need to remember the original plaintext (a useful feature for stateless systems).

Considering a simpler scenario, what if Alice wants to share the same secret message with Bob and Carlos?

graph LR; Alice-->|Message|Bob Alice-->|Message|Carlos

Naively, Alice could encrypt the message twice, once using Bob’s public key, and again using Carlos’. This would be fine, but introduces overhead and means that Alice has to keep track of which ciphertext was meant for which recipient.

Introducing eitherbox

eitherbox is a library built on top of crypto/nacl that aims to solve these problems.

It allows Alice to generate a single ciphertext which could be opened by either of any two parties, [herself or Bob] or [Bob or Carlos].

How does it work?

The concept is really simple. We generate a random shared key at the time of encryption, encrypt the real message with that key, and then encrypt the shared key for each of the recipients. This reduces the overhead involved by only having to duplicate the shared key rather than the entire ciphertext.

If Alice wants to share with Bob and Carlos:

graph LR; Alice-->|fa:fa-envelope Message|k{"fa:fa-key Random Key"} k-->B["fa:fa-key Bob"] k-->C["fa:fa-key Carlos"]

or if Alice want’s to keep her own copy of the ciphertext:

graph LR; Alice-->|fa:fa-envelope Message|k{"fa:fa-key Random Key"} k-->B["fa:fa-key Bob"] k-->Alice["fa:fa-key Alice"]

The raw structure of the data follows this pattern:

graph LR k1[["k1(k3)"]]-->k2[["k2(k3)"]] k2[["k2(k3)"]]-->ct[["k3(ciphertext)"]]

Where k1 is Bob’s public key, k2 is Carlos’ public key, and k3 is the randomly generated key. k(x) means x encrypted by k.

As you can see, given a key-pair, we can try both k1(k3) and k2(k3); if either of them can be decrypted, then we have k3 and can use it to decrypt ciphertext.

Example

import (
  "crypto/rand"
  "fmt"

  "github.com/mrobinsn/eitherbox"
  "golang.org/x/crypto/nacl/box"
)

func main() {
  // Create keys for Alice
  alicePublic, alicePrivate, _ := box.GenerateKey(rand.Reader)

  // Create keys for Bob
  bobPublic, bobPrivate, _ := box.GenerateKey(rand.Reader)

  // Create keys for Eve
  evePublic, evePrivate, _ := box.GenerateKey(rand.Reader)

  secret := []byte("hello world")

  ebox := eitherbox.Encrypt(secret, alicePublic, bobPublic)

  // Alice can decrypt
  aliceMsg, _ := ebox.Decrypt(alicePublic, alicePrivate)

  // Bob can decrypt
  bobMsg, _ := ebox.Decrypt(bobPublic, bobPrivate)

  // Eve can't decrypt
  eveMsg, _ := ebox.Decrypt(evePublic, evePrivate)

  fmt.Println("Alice got:", string(aliceMsg))
  fmt.Println("Bob got:", string(bobMsg))
  fmt.Println("Eve got:", string(eveMsg))
  // Output: Alice got: hello world
  // Bob got: hello world
  // Eve got:
}

How it could be extended

While eitherbox is set up to support two recipients, there is no practical limit to the number of recipients that this pattern could be used for. The overhead is constant for each additional recipient (80 bytes).

The pattern could easily be expanded to support N recipients:

graph LR; Alice-->|fa:fa-envelope Message|k{"fa:fa-key Random Key"} k-->A["fa:fa-key Recipient A"] k-->B["fa:fa-key Recipient B"] k-->C["fa:fa-key Recipient C"] k-->D["fa:fa-key Recipient ..."]
graph LR k1[["k1(k0)"]]-->k2[["k2(k0)"]] k2[["k2(k0)"]]-->k3[["..."]] k3[["..."]]-->k4[["kN(k0)"]] k4[["kN(k0)"]]-->ct[["k0(ciphertext)"]]

Disclaimers

While eitherbox is built on top of the proven NaCL primitives, eitherbox itself has not undergone an independent security audit. As such it should be used with care and it is your responsibility to understand the implications of using it in a production application.