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.
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?
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:
or if Alice want’s to keep her own copy of the ciphertext:
The raw structure of the data follows this pattern:
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:
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.