forked from micro/go-micro
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add secrets interface to config/secrets (micro#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 <asim@aslam.me>
- Loading branch information
Showing
5 changed files
with
363 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |