Repository: nats-io/nsc
Pull request #230 (2019-11-15 16:04:33)

encrypt req/sub for kv demo
internal -E and -D flags for nsc req to encrypt requests and decrypt responses.
internal flag -D for nsc sub to decrypt messages


Patch:
diff --git a/Makefile b/Makefile
index 3fe02bd..d9fb5b6 100644
--- a/Makefile
+++ b/Makefile
@@ -25,7 +25,7 @@ compile:
 	goreleaser --snapshot --rm-dist --skip-validate --skip-publish --parallelism 12
 
 install: compile build
-	cp $(BUILD_DIR)/$(BUILD_OS)_$(BUILD_OS_ARCH)/* $(BUILD_OS_GOPATH)/bin
+	cp $(BUILD_DIR)/nsc_$(BUILD_OS)_$(BUILD_OS_ARCH)/* $(BUILD_OS_GOPATH)/bin
 
 cover: test
 	go tool cover -html=./coverage.out
diff --git a/cmd/reqtool.go b/cmd/reqtool.go
index 2782cb6..b1b63c5 100644
--- a/cmd/reqtool.go
+++ b/cmd/reqtool.go
@@ -39,6 +39,10 @@ func createToolReqCmd() *cobra.Command {
 		},
 	}
 	params.BindFlags(cmd)
+	cmd.Flags().BoolVarP(&encryptFlag, "encrypt", "E", false, "encrypt payload")
+	cmd.Flags().MarkHidden("encrypt")
+	cmd.Flags().MarkHidden("decrypt")
+
 	return cmd
 }
 
@@ -89,6 +93,13 @@ func (p *ReqParams) Validate(ctx ActionCtx) error {
 		return err
 	}
 
+	if encryptFlag {
+		_, err := ctx.StoreCtx().KeyStore.GetSeed(ctx.StoreCtx().Account.PublicKey)
+		if err != nil {
+			return fmt.Errorf("unable to get the account private key to encrypt/decrypt the payload: %v", err)
+		}
+	}
+
 	if p.credsPath == "" {
 		return fmt.Errorf("a creds file for account %q/%q was not found", p.AccountContextParams.Name, p.UserContextParams.Name)
 	}
@@ -117,8 +128,23 @@ func (p *ReqParams) Run(ctx ActionCtx) (store.Status, error) {
 		payload = ctx.Args()[1]
 	}
 
-	ctx.CurrentCmd().Printf("published request: [%s] : '%s'\n", subj, payload)
-	msg, err := nc.Request(subj, []byte(payload), 5*time.Second)
+	var seed string
+	if encryptFlag {
+		// cannot fail if we are here
+		seed, _ = ctx.StoreCtx().KeyStore.GetSeed(ctx.StoreCtx().Account.PublicKey)
+	}
+
+	out := []byte(payload)
+	if encryptFlag {
+		out, err = EncryptKV(seed, out)
+	}
+
+	if encryptFlag {
+		ctx.CurrentCmd().Printf("published encrypted request: [%s] : '%s'\n", subj, payload)
+	} else {
+		ctx.CurrentCmd().Printf("published request: [%s] : '%s'\n", subj, payload)
+	}
+	msg, err := nc.Request(subj, out, 5*time.Second)
 	if err == nats.ErrTimeout {
 		ctx.CurrentCmd().Println("request timed out")
 		return nil, nil
@@ -127,6 +153,9 @@ func (p *ReqParams) Run(ctx ActionCtx) (store.Status, error) {
 		return nil, err
 	}
 
+	if encryptFlag {
+		msg = maybeDecryptMessage(seed, msg)
+	}
 	ctx.CurrentCmd().Printf("received reply: [%v] : '%s'\n", msg.Subject, string(msg.Data))
 	return nil, nil
 }
diff --git a/cmd/subtool.go b/cmd/subtool.go
index 78a1226..d4445f4 100644
--- a/cmd/subtool.go
+++ b/cmd/subtool.go
@@ -41,7 +41,10 @@ func createSubCmd() *cobra.Command {
 	}
 	cmd.Flags().StringVarP(&params.queue, "queue", "q", "", "subscription queue name")
 	cmd.Flags().IntVarP(&params.maxMessages, "max-messages", "", -1, "max messages")
+	cmd.Flags().BoolVarP(&encryptFlag, "encrypt", "E", false, "encrypted payload")
 	cmd.Flags().MarkHidden("max-messages")
+	cmd.Flags().MarkHidden("decrypt")
+
 	params.BindFlags(cmd)
 	return cmd
 }
@@ -148,6 +151,15 @@ func (p *SubParams) Run(ctx ActionCtx) (store.Status, error) {
 		return nil, err
 	}
 
+	var seed string
+	if encryptFlag {
+		// cannot fail if we are here
+		seed, err = ctx.StoreCtx().KeyStore.GetSeed(ctx.StoreCtx().Account.PublicKey)
+		if err != nil {
+			return nil, fmt.Errorf("unable to get the account private key to encrypt/decrypt the payload: %v", err)
+		}
+	}
+
 	i := 0
 	for {
 		msg, err := sub.NextMsg(10 * time.Second)
@@ -165,8 +177,31 @@ func (p *SubParams) Run(ctx ActionCtx) (store.Status, error) {
 		}
 
 		i++
-		ctx.CurrentCmd().Printf("[#%d] Received on [%s]: '%s'\n", i, msg.Subject, string(msg.Data))
+		if encryptFlag {
+			msg = maybeDecryptMessage(seed, msg)
+		}
+		ctx.CurrentCmd().Printf("[#%d] received on [%s]: '%s'\n", i, msg.Subject, string(msg.Data))
 	}
 
 	return nil, nil
 }
+
+func maybeDecryptMessage(seed string, msg *nats.Msg) *nats.Msg {
+	var dmsg nats.Msg
+	// last part of the subject will be encrypted
+	tokens := strings.Split(msg.Subject, ".")
+	k := tokens[len(tokens)-1]
+	kk, err := Decrypt(seed, []byte(k))
+	if err != nil {
+		dmsg.Subject = msg.Subject
+	} else {
+		tokens[len(tokens)-1] = string(kk)
+		dmsg.Subject = strings.Join(tokens, ".")
+	}
+
+	dmsg.Data, err = Decrypt(seed, msg.Data)
+	if err != nil {
+		dmsg.Data = msg.Data
+	}
+	return &dmsg
+}
diff --git a/cmd/tool.go b/cmd/tool.go
index e0e8715..6d69951 100644
--- a/cmd/tool.go
+++ b/cmd/tool.go
@@ -16,6 +16,15 @@
 package cmd
 
 import (
+	"bytes"
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/sha256"
+	"encoding/base64"
+	"errors"
+	"fmt"
+	"io"
+	"strings"
 	"time"
 
 	nats "github.com/nats-io/nats.go"
@@ -28,6 +37,7 @@ var toolCmd = &cobra.Command{
 }
 
 var natsURLFlag = ""
+var encryptFlag bool
 
 func init() {
 	toolCmd.PersistentFlags().StringVarP(&natsURLFlag, "nats", "", "", "nats url, defaults to the operator's service URLs")
@@ -63,3 +73,100 @@ func createDefaultToolOptions(name string, ctx ActionCtx) []nats.Option {
 	}))
 	return opts
 }
+
+func createCypher(pk string) (cipher.AEAD, error) {
+	// hash the provided private nkey into 32 bytes
+	hash := sha256.Sum256([]byte(pk))
+	c, err := aes.NewCipher(hash[:32])
+	if err != nil {
+		return nil, fmt.Errorf("unable to generate cypher: %v", err)
+	}
+
+	// create the symmetric key cipher
+	return cipher.NewGCM(c)
+}
+
+func EncryptKV(pk string, data []byte) ([]byte, error) {
+	// source data is <k><space><v>
+	i := bytes.IndexByte(data, ' ')
+	if i == -1 {
+		k, err := Encrypt(pk, data)
+		if err != nil {
+			return nil, err
+		}
+		return k, nil
+	}
+	// kv pair
+	k, err := Encrypt(pk, data[:i])
+	if err != nil {
+		return nil, err
+	}
+	v, err := Encrypt(pk, data[i+1:])
+	if err != nil {
+		return nil, err
+	}
+	return bytes.Join([][]byte{k, v}, []byte(" ")), nil
+}
+
+func Encrypt(pk string, data []byte) ([]byte, error) {
+	g, err := createCypher(pk)
+	if err != nil {
+		return nil, err
+	}
+	// creates a byte array the size of the nonce required
+	nonce := make([]byte, g.NonceSize())
+	// seed the nonce with the same seed so that we have predictable encryption
+	if _, err = io.ReadFull(strings.NewReader(pk), nonce); err != nil {
+		return nil, fmt.Errorf("error generating random sequence: %v", err)
+	}
+	// encrypt the data
+	raw := g.Seal(nonce, nonce, data, nil)
+
+	// encode the data
+	var codec = base64.StdEncoding.WithPadding(base64.NoPadding)
+	buf := make([]byte, codec.EncodedLen(len(raw)))
+	codec.Encode(buf, raw)
+	return buf[:], nil
+}
+
+func Decrypt(pk string, data []byte) ([]byte, error) {
+	// response payloads may be encrypted or may be lists of values separated by a space
+	if bytes.IndexByte(data, ' ') != -1 {
+		var decoded [][]byte
+		for _, a := range bytes.Split(data, []byte(" ")) {
+			d, err := decrypt(pk, a)
+			if err != nil {
+				return nil, err
+			}
+			decoded = append(decoded, d)
+		}
+		return bytes.Join(decoded, []byte(" ")), nil
+	} else {
+		return decrypt(pk, data)
+	}
+}
+
+func decrypt(pk string, data []byte) ([]byte, error) {
+	var codec = base64.StdEncoding.WithPadding(base64.NoPadding)
+	raw := make([]byte, codec.DecodedLen(len(data)))
+	n, err := codec.Decode(raw, data)
+	if err != nil {
+		if _, ok := err.(base64.CorruptInputError); ok {
+			// possibly not encrypted - so just return what we got
+			return data, nil
+		}
+		return nil, err
+	}
+	raw = raw[:n]
+
+	g, err := createCypher(pk)
+	if err != nil {
+		return nil, err
+	}
+	nonceLen := g.NonceSize()
+	if nonceLen > len(raw) {
+		return nil, errors.New("unexpected data length")
+	}
+	nonce, cypher := raw[:nonceLen], raw[nonceLen:]
+	return g.Open(nil, nonce, cypher, nil)
+}
diff --git a/go.mod b/go.mod
index b4ec6c2..3cdd342 100644
--- a/go.mod
+++ b/go.mod
@@ -19,10 +19,10 @@ require (
 	github.com/mitchellh/go-homedir v1.0.0
 	github.com/mitchellh/go-wordwrap v1.0.0
 	github.com/mitchellh/mapstructure v1.1.2 // indirect
-	github.com/nats-io/jwt v0.3.1-0.20190920180816-3289d6f980ad
+	github.com/nats-io/jwt v0.3.2
 	github.com/nats-io/nats-server/v2 v2.0.1-0.20190625001713-2db76bde3329
 	github.com/nats-io/nats.go v1.8.1
-	github.com/nats-io/nkeys v0.1.0
+	github.com/nats-io/nkeys v0.1.3
 	github.com/nats-io/nuid v1.0.1
 	github.com/onsi/ginkgo v1.7.0 // indirect
 	github.com/onsi/gomega v1.4.3 // indirect
diff --git a/go.sum b/go.sum
index 1ea739a..8d7cc53 100644
--- a/go.sum
+++ b/go.sum
@@ -64,16 +64,16 @@ github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQz
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/nats-io/jwt v0.2.6 h1:eAyoYvGgGLXR2EpnsBUvi/FcFrBqN6YKFVbOoEfPN4k=
 github.com/nats-io/jwt v0.2.6/go.mod h1:mQxQ0uHQ9FhEVPIcTSKwx2lqZEpXWWcCgA7R6NrWvvY=
-github.com/nats-io/jwt v0.3.1-0.20190920180816-3289d6f980ad h1:3JS/Fxo8s+wfFdaYeIMPgIYDq2WsgTvjFEjXAUoUXtI=
-github.com/nats-io/jwt v0.3.1-0.20190920180816-3289d6f980ad/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg=
+github.com/nats-io/jwt v0.3.2 h1:+RB5hMpXUUA2dfxuhBTEkMOrYmM+gKIZYS1KjSostMI=
+github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU=
 github.com/nats-io/nats-server/v2 v2.0.1-0.20190625001713-2db76bde3329 h1:RjMdWlS/avIVeXLjWxrHmxoiNPcvLXVWqpO7Bp6hyTk=
 github.com/nats-io/nats-server/v2 v2.0.1-0.20190625001713-2db76bde3329/go.mod h1:RyVdsHHvY4B6c9pWG+uRLpZ0h0XsqiuKp2XCTurP5LI=
 github.com/nats-io/nats.go v1.8.1 h1:6lF/f1/NN6kzUDBz6pyvQDEXO39jqXcWRLu/tKjtOUQ=
 github.com/nats-io/nats.go v1.8.1/go.mod h1:BrFz9vVn0fU3AcH9Vn4Kd7W0NpJ651tD5omQ3M8LwxM=
 github.com/nats-io/nkeys v0.0.2 h1:+qM7QpgXnvDDixitZtQUBDY9w/s9mu1ghS+JIbsrx6M=
 github.com/nats-io/nkeys v0.0.2/go.mod h1:dab7URMsZm6Z/jp9Z5UGa87Uutgc2mVpXLC4B7TDb/4=
-github.com/nats-io/nkeys v0.1.0 h1:qMd4+pRHgdr1nAClu+2h/2a5F2TmKcCzjCDazVgRoX4=
-github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
+github.com/nats-io/nkeys v0.1.3 h1:6JrEfig+HzTH85yxzhSVbjHRJv9cn0p6n3IngIcM5/k=
+github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
 github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
 github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw=


Test patch:
diff --git a/cmd/tools_test.go b/cmd/tools_test.go
index e35fe07..746e2c0 100644
--- a/cmd/tools_test.go
+++ b/cmd/tools_test.go
@@ -128,7 +128,7 @@ func TestSub(t *testing.T) {
 		t.Log(r.stderr)
 		t.Log(r.err)
 		require.NoError(t, r.err)
-		require.Contains(t, r.stderr, fmt.Sprintf("Received on [%s]: '%s'", v, v))
+		require.Contains(t, r.stderr, fmt.Sprintf("received on [%s]: '%s'", v, v))
 	case <-time.After(25 * time.Second):
 		t.Fatal("timed out")
 	}
@@ -224,3 +224,16 @@ func TestReply(t *testing.T) {
 	require.NoError(t, err)
 	require.Equal(t, v, string(m.Data))
 }
+
+func Test_EncryptDecrypt(t *testing.T) {
+	ts := NewTestStore(t, "O")
+	defer ts.Done(t)
+
+	k := ts.GetOperatorPublicKey(t)
+	text := "this is a test"
+	et, err := Encrypt(k, []byte(text))
+	require.NoError(t, err)
+	od, err := Decrypt(k, et)
+	require.NoError(t, err)
+	require.Equal(t, text, string(od))
+}