From becbf705a91575484002d598f87d74f0002801e7 Mon Sep 17 00:00:00 2001 From: Ben Toews Date: Wed, 4 Dec 2019 20:12:52 +0000 Subject: [PATCH] nacl/box: support anonymous seal/open This adds SealAnonymous and OpenAnonymous functions that implement the libsodium "sealed box" functionality. Fixes golang/go#35346 Change-Id: I22455f1b83595ec8a68d1861e635bd6cb0573f44 GitHub-Last-Rev: 7d334cf861942ec63ad613b7f28fb6dd7a1f9992 GitHub-Pull-Request: golang/crypto#107 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/205241 Reviewed-by: Filippo Valsorda Run-TryBot: Filippo Valsorda TryBot-Result: Gobot Gobot --- nacl/box/box.go | 83 +++++++++++++++++++++++++++++++++- nacl/box/box_test.go | 103 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 2 deletions(-) diff --git a/nacl/box/box.go b/nacl/box/box.go index 31b697b..7f3b830 100644 --- a/nacl/box/box.go +++ b/nacl/box/box.go @@ -31,19 +31,30 @@ Thus large amounts of data should be chunked so that each message is small. chunk size. This package is interoperable with NaCl: https://nacl.cr.yp.to/box.html. +Anonymous sealing/opening is an extension of NaCl defined by and interoperable +with libsodium: +https://libsodium.gitbook.io/doc/public-key_cryptography/sealed_boxes. */ package box // import "golang.org/x/crypto/nacl/box" import ( + cryptorand "crypto/rand" "io" + "golang.org/x/crypto/blake2b" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/nacl/secretbox" "golang.org/x/crypto/salsa20/salsa" ) -// Overhead is the number of bytes of overhead when boxing a message. -const Overhead = secretbox.Overhead +const ( + // Overhead is the number of bytes of overhead when boxing a message. + Overhead = secretbox.Overhead + + // AnonymousOverhead is the number of bytes of overhead when using anonymous + // sealed boxes. + AnonymousOverhead = Overhead + 32 +) // GenerateKey generates a new public/private key pair suitable for use with // Seal and Open. @@ -101,3 +112,71 @@ func Open(out, box []byte, nonce *[24]byte, peersPublicKey, privateKey *[32]byte func OpenAfterPrecomputation(out, box []byte, nonce *[24]byte, sharedKey *[32]byte) ([]byte, bool) { return secretbox.Open(out, box, nonce, sharedKey) } + +// SealAnonymous appends an encrypted and authenticated copy of message to out, +// which will be AnonymousOverhead bytes longer than the original and must not +// overlap it. This differs from Seal in that the sender is not required to +// provide a private key. +func SealAnonymous(out, message []byte, recipient *[32]byte, rand io.Reader) ([]byte, error) { + if rand == nil { + rand = cryptorand.Reader + } + ephemeralPub, ephemeralPriv, err := GenerateKey(rand) + if err != nil { + return nil, err + } + + var nonce [24]byte + if err := sealNonce(ephemeralPub, recipient, &nonce); err != nil { + return nil, err + } + + if total := len(out) + AnonymousOverhead + len(message); cap(out) < total { + original := out + out = make([]byte, 0, total) + out = append(out, original...) + } + out = append(out, ephemeralPub[:]...) + + return Seal(out, message, &nonce, recipient, ephemeralPriv), nil +} + +// OpenAnonymous authenticates and decrypts a box produced by SealAnonymous and +// appends the message to out, which must not overlap box. The output will be +// AnonymousOverhead bytes smaller than box. +func OpenAnonymous(out, box []byte, publicKey, privateKey *[32]byte) (message []byte, ok bool) { + if len(box) < AnonymousOverhead { + return nil, false + } + + var ephemeralPub [32]byte + copy(ephemeralPub[:], box[:32]) + + var nonce [24]byte + if err := sealNonce(&ephemeralPub, publicKey, &nonce); err != nil { + return nil, false + } + + return Open(out, box[32:], &nonce, &ephemeralPub, privateKey) +} + +// sealNonce generates a 24 byte nonce that is a blake2b digest of the +// ephemeral public key and the receiver's public key. +func sealNonce(ephemeralPub, peersPublicKey *[32]byte, nonce *[24]byte) error { + h, err := blake2b.New(24, nil) + if err != nil { + return err + } + + if _, err = h.Write(ephemeralPub[:]); err != nil { + return err + } + + if _, err = h.Write(peersPublicKey[:]); err != nil { + return err + } + + h.Sum(nonce[:0]) + + return nil +} diff --git a/nacl/box/box_test.go b/nacl/box/box_test.go index 481ade2..cce1f3b 100644 --- a/nacl/box/box_test.go +++ b/nacl/box/box_test.go @@ -76,3 +76,106 @@ func TestBox(t *testing.T) { t.Fatalf("box didn't match, got\n%x\n, expected\n%x", box, expected) } } + +func TestSealOpenAnonymous(t *testing.T) { + publicKey, privateKey, _ := GenerateKey(rand.Reader) + message := []byte("test message") + + box, err := SealAnonymous(nil, message, publicKey, nil) + if err != nil { + t.Fatalf("Unexpected error sealing %v", err) + } + opened, ok := OpenAnonymous(nil, box, publicKey, privateKey) + if !ok { + t.Fatalf("failed to open box") + } + + if !bytes.Equal(opened, message) { + t.Fatalf("got %x, want %x", opened, message) + } + + for i := range box { + box[i] ^= 0x40 + _, ok := OpenAnonymous(nil, box, publicKey, privateKey) + if ok { + t.Fatalf("opened box with byte %d corrupted", i) + } + box[i] ^= 0x40 + } + + // allocates new slice if out isn't long enough + out := []byte("hello") + orig := append([]byte(nil), out...) + box, err = SealAnonymous(out, message, publicKey, nil) + if err != nil { + t.Fatalf("Unexpected error sealing %v", err) + } + if !bytes.Equal(out, orig) { + t.Fatal("expected out to be unchanged") + } + if !bytes.HasPrefix(box, orig) { + t.Fatal("expected out to be coppied to returned slice") + } + _, ok = OpenAnonymous(nil, box[len(out):], publicKey, privateKey) + if !ok { + t.Fatalf("failed to open box") + } + + // uses provided slice if it's long enough + out = append(make([]byte, 0, 1000), []byte("hello")...) + orig = append([]byte(nil), out...) + box, err = SealAnonymous(out, message, publicKey, nil) + if err != nil { + t.Fatalf("Unexpected error sealing %v", err) + } + if !bytes.Equal(out, orig) { + t.Fatal("expected out to be unchanged") + } + if &out[0] != &box[0] { + t.Fatal("expected box to point to out") + } + _, ok = OpenAnonymous(nil, box[len(out):], publicKey, privateKey) + if !ok { + t.Fatalf("failed to open box") + } +} + +func TestSealedBox(t *testing.T) { + var privateKey [32]byte + for i := range privateKey[:] { + privateKey[i] = 1 + } + + var publicKey [32]byte + curve25519.ScalarBaseMult(&publicKey, &privateKey) + var message [64]byte + for i := range message[:] { + message[i] = 3 + } + + fakeRand := bytes.NewReader([]byte{5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5}) + box, err := SealAnonymous(nil, message[:], &publicKey, fakeRand) + if err != nil { + t.Fatalf("Unexpected error sealing %v", err) + } + + // expected was generated using the C implementation of libsodium with a + // random implementation that always returns 5. + // https://gist.github.com/mastahyeti/942ec3f175448d68fed25018adbce5a7 + expected, _ := hex.DecodeString("50a61409b1ddd0325e9b16b700e719e9772c07000b1bd7786e907c653d20495d2af1697137a53b1b1dfc9befc49b6eeb38f86be720e155eb2be61976d2efb34d67ecd44a6ad634625eb9c288bfc883431a84ab0f5557dfe673aa6f74c19f033e648a947358cfcc606397fa1747d5219a") + + if !bytes.Equal(box, expected) { + t.Fatalf("box didn't match, got\n%x\n, expected\n%x", box, expected) + } + + // box was generated using the C implementation of libsodium. + // https://gist.github.com/mastahyeti/942ec3f175448d68fed25018adbce5a7 + box, _ = hex.DecodeString("3462e0640728247a6f581e3812850d6edc3dcad1ea5d8184c072f62fb65cb357e27ffa8b76f41656bc66a0882c4d359568410665746d27462a700f01e314f382edd7aae9064879b0f8ba7b88866f88f5e4fbd7649c850541877f9f33ebd25d46d9cbcce09b69a9ba07f0eb1d105d4264") + result, ok := OpenAnonymous(nil, box, &publicKey, &privateKey) + if !ok { + t.Fatalf("failed to open box") + } + if !bytes.Equal(result, message[:]) { + t.Fatalf("message didn't match, got\n%x\n, expected\n%x", result, message[:]) + } +}