From 4125ae8d5324102549223b40ca9258785906314d Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Tue, 10 Mar 2020 22:52:06 +0000 Subject: [PATCH] Add secrets interface to config/secrets (#1325) * Interface for secrets * Add secretbox secrets implementation * Start working on box * typo * Add asymmetric encryption implementation * go mod tidy * Fix review comments Co-authored-by: Asim Aslam --- config/secrets/box/box.go | 89 ++++++++++++++++++++++ config/secrets/box/box_test.go | 63 +++++++++++++++ config/secrets/secretbox/secretbox.go | 73 ++++++++++++++++++ config/secrets/secretbox/secretbox_test.go | 56 ++++++++++++++ config/secrets/secrets.go | 82 ++++++++++++++++++++ 5 files changed, 363 insertions(+) create mode 100644 config/secrets/box/box.go create mode 100644 config/secrets/box/box_test.go create mode 100644 config/secrets/secretbox/secretbox.go create mode 100644 config/secrets/secretbox/secretbox_test.go create mode 100644 config/secrets/secrets.go diff --git a/config/secrets/box/box.go b/config/secrets/box/box.go new file mode 100644 index 00000000..54192c3d --- /dev/null +++ b/config/secrets/box/box.go @@ -0,0 +1,89 @@ +// Package box is an asymmetric implementation of config/secrets using nacl/box +package box + +import ( + "github.com/micro/go-micro/v2/config/secrets" + "github.com/pkg/errors" + naclbox "golang.org/x/crypto/nacl/box" + + "crypto/rand" +) + +const keyLength = 32 + +type box struct { + options secrets.Options + + publicKey [keyLength]byte + privateKey [keyLength]byte +} + +// NewCodec returns a nacl-box codec +func NewCodec(opts ...secrets.Option) secrets.Codec { + b := &box{} + for _, o := range opts { + o(&b.options) + } + return b +} + +// Init initialises a box +func (b *box) Init(opts ...secrets.Option) error { + for _, o := range opts { + o(&b.options) + } + if len(b.options.PrivateKey) != keyLength || len(b.options.PublicKey) != keyLength { + return errors.Errorf("a public key and a private key of length %d must both be provided", keyLength) + } + copy(b.privateKey[:], b.options.PrivateKey) + copy(b.publicKey[:], b.options.PublicKey) + return nil +} + +// Options returns options +func (b *box) Options() secrets.Options { + return b.options +} + +// String returns nacl-box +func (*box) String() string { + return "nacl-box" +} + +// Encrypt encrypts a message with the sender's private key and the receipient's public key +func (b *box) Encrypt(in []byte, opts ...secrets.EncryptOption) ([]byte, error) { + var options secrets.EncryptOptions + for _, o := range opts { + o(&options) + } + if len(options.RecipientPublicKey) != keyLength { + return []byte{}, errors.New("recepient's public key must be provided") + } + var recipientPublicKey [keyLength]byte + copy(recipientPublicKey[:], options.RecipientPublicKey) + var nonce [24]byte + if _, err := rand.Reader.Read(nonce[:]); err != nil { + return []byte{}, errors.Wrap(err, "couldn't obtain a random nonce from crypto/rand") + } + return naclbox.Seal(nonce[:], in, &nonce, &recipientPublicKey, &b.privateKey), nil +} + +// Decrypt Decrypts a message with the receiver's private key and the sender's public key +func (b *box) Decrypt(in []byte, opts ...secrets.DecryptOption) ([]byte, error) { + var options secrets.DecryptOptions + for _, o := range opts { + o(&options) + } + if len(options.SenderPublicKey) != keyLength { + return []byte{}, errors.New("sender's public key bust be provided") + } + var nonce [24]byte + var senderPublicKey [32]byte + copy(nonce[:], in[:24]) + copy(senderPublicKey[:], options.SenderPublicKey) + decrypted, ok := naclbox.Open(nil, in[24:], &nonce, &senderPublicKey, &b.privateKey) + if !ok { + return []byte{}, errors.New("incoming message couldn't be verified / decrypted") + } + return decrypted, nil +} diff --git a/config/secrets/box/box_test.go b/config/secrets/box/box_test.go new file mode 100644 index 00000000..77a5559a --- /dev/null +++ b/config/secrets/box/box_test.go @@ -0,0 +1,63 @@ +package box + +import ( + "crypto/rand" + "reflect" + "testing" + + "github.com/micro/go-micro/v2/config/secrets" + naclbox "golang.org/x/crypto/nacl/box" +) + +func TestBox(t *testing.T) { + alicePublicKey, alicePrivateKey, err := naclbox.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + bobPublicKey, bobPrivateKey, err := naclbox.GenerateKey(rand.Reader) + if err != nil { + t.Fatal(err) + } + alice, bob := NewCodec(secrets.PublicKey(alicePublicKey[:]), secrets.PrivateKey(alicePrivateKey[:])), NewCodec() + if err := alice.Init(); err != nil { + t.Error(err) + } + if err := bob.Init(secrets.PublicKey(bobPublicKey[:]), secrets.PrivateKey(bobPrivateKey[:])); err != nil { + t.Error(err) + } + if alice.String() != "nacl-box" { + t.Error("String() doesn't return nacl-box") + } + aliceSecret := []byte("Why is a raven like a writing-desk?") + if _, err := alice.Encrypt(aliceSecret); err == nil { + t.Error("alice.Encrypt succeded without a public key") + } + enc, err := alice.Encrypt(aliceSecret, secrets.RecipientPublicKey(bob.Options().PublicKey)) + if err != nil { + t.Error("alice.Encrypt failed") + } + if _, err := bob.Decrypt(enc); err == nil { + t.Error("bob.Decrypt succeded without a public key") + } + if dec, err := bob.Decrypt(enc, secrets.SenderPublicKey(alice.Options().PublicKey)); err == nil { + if !reflect.DeepEqual(dec, aliceSecret) { + t.Errorf("Bob's decrypted message didn't match Alice's encrypted message: %v != %v", aliceSecret, dec) + } + } else { + t.Errorf("bob.Decrypt failed (%s)", err) + } + + bobSecret := []byte("I haven't the slightest idea") + enc, err = bob.Encrypt(bobSecret, secrets.RecipientPublicKey(alice.Options().PublicKey)) + if err != nil { + t.Error(err) + } + dec, err := alice.Decrypt(enc, secrets.SenderPublicKey(bob.Options().PrivateKey)) + if err == nil { + t.Error(err) + } + dec, err = alice.Decrypt(enc, secrets.SenderPublicKey(bob.Options().PublicKey)) + if !reflect.DeepEqual(dec, bobSecret) { + t.Errorf("Alice's decrypted message didn't match Bob's encrypted message %v != %v", bobSecret, dec) + } +} diff --git a/config/secrets/secretbox/secretbox.go b/config/secrets/secretbox/secretbox.go new file mode 100644 index 00000000..31c36ea3 --- /dev/null +++ b/config/secrets/secretbox/secretbox.go @@ -0,0 +1,73 @@ +// Package secretbox is a config/secrets implementation that uses nacl/secretbox +// to do symmetric encryption / verification +package secretbox + +import ( + "github.com/micro/go-micro/v2/config/secrets" + "github.com/pkg/errors" + "golang.org/x/crypto/nacl/secretbox" + + "crypto/rand" +) + +const keyLength = 32 + +type secretBox struct { + options secrets.Options + + secretKey [keyLength]byte +} + +// NewCodec returns a secretbox codec +func NewCodec(opts ...secrets.Option) secrets.Codec { + sb := &secretBox{} + for _, o := range opts { + o(&sb.options) + } + return sb +} + +func (s *secretBox) Init(opts ...secrets.Option) error { + for _, o := range opts { + o(&s.options) + } + if len(s.options.SecretKey) == 0 { + return errors.New("no secret key is defined") + } + if len(s.options.SecretKey) != keyLength { + return errors.Errorf("secret key must be %d bytes long", keyLength) + } + copy(s.secretKey[:], s.options.SecretKey) + return nil +} + +func (s *secretBox) Options() secrets.Options { + return s.options +} + +func (s *secretBox) String() string { + return "nacl-secretbox" +} + +func (s *secretBox) Encrypt(in []byte, opts ...secrets.EncryptOption) ([]byte, error) { + // no opts are expected, so they are ignored + + // there must be a unique nonce for each message + var nonce [24]byte + if _, err := rand.Reader.Read(nonce[:]); err != nil { + return []byte{}, errors.Wrap(err, "couldn't obtain a random nonce from crypto/rand") + } + return secretbox.Seal(nonce[:], in, &nonce, &s.secretKey), nil +} + +func (s *secretBox) Decrypt(in []byte, opts ...secrets.DecryptOption) ([]byte, error) { + // no options are expected, so they are ignored + + var decryptNonce [24]byte + copy(decryptNonce[:], in[:24]) + decrypted, ok := secretbox.Open(nil, in[24:], &decryptNonce, &s.secretKey) + if !ok { + return []byte{}, errors.New("decryption failed (is the key set correctly?)") + } + return decrypted, nil +} diff --git a/config/secrets/secretbox/secretbox_test.go b/config/secrets/secretbox/secretbox_test.go new file mode 100644 index 00000000..04c3a2f5 --- /dev/null +++ b/config/secrets/secretbox/secretbox_test.go @@ -0,0 +1,56 @@ +package secretbox + +import ( + "encoding/base64" + "reflect" + "testing" + + "github.com/micro/go-micro/v2/config/secrets" +) + +func TestSecretBox(t *testing.T) { + secretKey, err := base64.StdEncoding.DecodeString("4jbVgq8FsAV7vy+n8WqEZrl7BUtNqh3fYT5RXzXOPFY=") + if err != nil { + t.Fatal(err) + } + + s := NewCodec() + + if err := s.Init(); err == nil { + t.Error("Secretbox accepted an empty secret key") + } + if err := s.Init(secrets.SecretKey([]byte("invalid"))); err == nil { + t.Error("Secretbox accepted a secret key that is invalid") + } + + if err := s.Init(secrets.SecretKey(secretKey)); err != nil { + t.Fatal(err) + } + + o := s.Options() + if !reflect.DeepEqual(o.SecretKey, secretKey) { + t.Error("Init() didn't set secret key correctly") + } + if s.String() != "nacl-secretbox" { + t.Error(s.String() + " should be nacl-secretbox") + } + + // Try 10 times to get different nonces + for i := 0; i < 10; i++ { + message := []byte(`Can you hear me, Major Tom?`) + + encrypted, err := s.Encrypt(message) + if err != nil { + t.Errorf("Failed to encrypt message (%s)", err) + } + + decrypted, err := s.Decrypt(encrypted) + if err != nil { + t.Errorf("Failed to decrypt encrypted message (%s)", err) + } + + if !reflect.DeepEqual(message, decrypted) { + t.Errorf("Decrypted Message dod not match encrypted message") + } + } +} diff --git a/config/secrets/secrets.go b/config/secrets/secrets.go new file mode 100644 index 00000000..b2ec4c07 --- /dev/null +++ b/config/secrets/secrets.go @@ -0,0 +1,82 @@ +// Package secrets is an interface for encrypting and decrypting secrets +package secrets + +import "context" + +// Codec encrypts or decrypts arbitrary data. The data should be as small as possible +type Codec interface { + Init(...Option) error + Options() Options + String() string + Decrypt([]byte, ...DecryptOption) ([]byte, error) + Encrypt([]byte, ...EncryptOption) ([]byte, error) +} + +// Options is a codec's options +// SecretKey or both PublicKey and PrivateKey should be set depending on the +// underlying implementation +type Options struct { + SecretKey []byte + PrivateKey []byte + PublicKey []byte + Context context.Context +} + +// Option sets options +type Option func(*Options) + +// SecretKey sets the symmetric secret key +func SecretKey(key []byte) Option { + return func(o *Options) { + o.SecretKey = make([]byte, len(key)) + copy(o.SecretKey, key) + } +} + +// PublicKey sets the asymmetric Public Key of this codec +func PublicKey(key []byte) Option { + return func(o *Options) { + o.PublicKey = make([]byte, len(key)) + copy(o.PublicKey, key) + } +} + +// PrivateKey sets the asymmetric Private Key of this codec +func PrivateKey(key []byte) Option { + return func(o *Options) { + o.PrivateKey = make([]byte, len(key)) + copy(o.PrivateKey, key) + } +} + +// DecryptOptions can be passed to Codec.Decrypt +type DecryptOptions struct { + SenderPublicKey []byte +} + +// DecryptOption sets DecryptOptions +type DecryptOption func(*DecryptOptions) + +// SenderPublicKey is the Public Key of the Codec that encrypted this message +func SenderPublicKey(key []byte) DecryptOption { + return func(d *DecryptOptions) { + d.SenderPublicKey = make([]byte, len(key)) + copy(d.SenderPublicKey, key) + } +} + +// EncryptOptions can be passed to Codec.Encrypt +type EncryptOptions struct { + RecipientPublicKey []byte +} + +// EncryptOption Sets EncryptOptions +type EncryptOption func(*EncryptOptions) + +// RecipientPublicKey is the Public Key of the Codec that will decrypt this message +func RecipientPublicKey(key []byte) EncryptOption { + return func(e *EncryptOptions) { + e.RecipientPublicKey = make([]byte, len(key)) + copy(e.RecipientPublicKey, key) + } +}