Skip to content
This repository has been archived by the owner on May 24, 2024. It is now read-only.

Commit

Permalink
Rotate as a service (#9)
Browse files Browse the repository at this point in the history
* Implement Rotate

* clean up

* Version bump for next release
  • Loading branch information
epk authored Jun 10, 2021
1 parent f5b95f6 commit 31fc064
Show file tree
Hide file tree
Showing 1,082 changed files with 245 additions and 366,755 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ endif
all: fmt test build

build:
GOOS=$(OS) GOARCH="$(GOARCH)" go build -mod vendor -o vault/plugins/secrets-ejson cmd/vault-plugin-secrets-ejson/main.go
GOOS=$(OS) GOARCH="$(GOARCH)" go build -trimpath -ldflags "-s -w" -o vault/plugins/secrets-ejson cmd/vault-plugin-secrets-ejson/main.go

clean:
rm -f ./vault-plugin-secrets-ejson
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.1.0
1.2.0
1 change: 1 addition & 0 deletions backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func Backend() *backend {
b.Backend = &framework.Backend{
Help: "",
Paths: framework.PathAppend(
ejsonRotatePaths(&b),
ejsonAnalysePaths(&b),
ejsonIdentityPath(&b),
ejsonDecryptPaths(&b),
Expand Down
2 changes: 1 addition & 1 deletion dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ display_name: "Vault Plugin: Secrets EJSON"

up:
- go:
version: 1.13.5
version: 1.16

commands:
make:deps:
Expand Down
6 changes: 2 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
module github.com/Shopify/vault-plugin-secrets-ejson

go 1.13
go 1.16

require (
github.com/Shopify/ejson v1.2.1
github.com/Shopify/ejson v1.2.2
github.com/armon/go-radix v1.0.0 // indirect
github.com/dustin/gojson v0.0.0-20160307161227-2e71ec9dd5ad // indirect
github.com/fatih/color v1.8.0 // indirect
github.com/frankban/quicktest v1.7.2 // indirect
github.com/go-test/deep v1.0.4 // indirect
Expand All @@ -18,7 +17,6 @@ require (
github.com/hashicorp/vault/sdk v0.1.14-0.20191218020134-06959d23b502
github.com/mattn/go-isatty v0.0.11 // indirect
github.com/pierrec/lz4 v2.4.0+incompatible // indirect
github.com/smartystreets/goconvey v1.6.4 // indirect
golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64 // indirect
)
49 changes: 16 additions & 33 deletions go.sum

Large diffs are not rendered by default.

39 changes: 38 additions & 1 deletion helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,26 @@ import (
"golang.org/x/crypto/scrypt"
)

func DecryptEjsonDocument(ctx context.Context, req *logical.Request, encData []byte) (map[string]interface{}, error) {
func EncryptEjsonDocument(ctx context.Context, decData map[string]interface{}) (map[string]interface{}, error) {
decBytes, err := MarshalForEjson(decData)
if err != nil {
return nil, fmt.Errorf("failed to marshal json: %s", err)
}

encBytes, err := EncryptEjson(ctx, decBytes)
if err != nil {
return nil, err
}

encData := map[string]interface{}{}
if err := json.Unmarshal(encBytes, &encData); err != nil {
return nil, err
}

return encData, nil
}

func DecryptEjsonDocument(ctx context.Context, req *logical.Request, encData []byte) (map[string]interface{}, error) {
decBytes, err := DecryptEjson(ctx, encData, req.Storage)
if err != nil {
return nil, err
Expand All @@ -26,6 +44,17 @@ func DecryptEjsonDocument(ctx context.Context, req *logical.Request, encData []b
return decData, nil
}

func EncryptEjson(ctx context.Context, decData []byte) ([]byte, error) {
var out bytes.Buffer

_, err := ejson.Encrypt(bytes.NewReader(decData), &out)
if err != nil {
return nil, fmt.Errorf("failed to encrypt ejson: %s", err)
}

return out.Bytes(), nil
}

func DecryptEjson(ctx context.Context, encData []byte, storage logical.Storage) ([]byte, error) {
var out bytes.Buffer
var err error
Expand Down Expand Up @@ -54,6 +83,14 @@ func MarshalInput(inputData interface{}) ([]byte, error) {
switch value := inputData.(type) {
case map[string]interface{}:
return MarshalForEjson(value)
case string:
d := map[string]interface{}{}
err := json.Unmarshal([]byte(value), &d)
if err != nil {
return nil, err
}

return MarshalForEjson(d)
default:
return nil, fmt.Errorf("data provided was in an unexpected format")
}
Expand Down
79 changes: 79 additions & 0 deletions path_ejson_rotate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package secretsejson

import (
"context"
"fmt"

"github.com/Shopify/ejson"
ej "github.com/Shopify/ejson/json"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)

func ejsonRotatePaths(b *backend) []*framework.Path {
return []*framework.Path{
&framework.Path{
Pattern: "rotate",
Fields: map[string]*framework.FieldSchema{
"document": &framework.FieldSchema{
Type: framework.TypeString,
Description: "EJSON Document",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.CreateOperation: b.rotate,
logical.UpdateOperation: b.rotate,
},
},
}
}

func (b *backend) rotate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
inputData, ok := data.GetOk("document")
if !ok {
if len(data.Raw) == 0 {
return logical.ErrorResponse("no data provided"), logical.ErrInvalidRequest
}
inputData = data.Raw
}

encData, err := MarshalInput(inputData)
if err != nil {
return nil, errwrap.Wrapf("failed to marshal json: {{err}}", err)
}

decDoc, err := DecryptEjsonDocument(ctx, req, encData)
if err != nil {
return nil, errwrap.Wrapf("failed to decrypt ejson: {{err}}", err)
}

public, private, err := ejson.GenerateKeypair()
if err != nil {
return nil, errwrap.Wrapf("failed to generate keypair ejson: {{err}}", err)
}

path := fmt.Sprintf("keys/%s", public)
b.Logger().Info(fmt.Sprintf("New key pair at %s", path))
entry := &logical.StorageEntry{
Key: path,
Value: []byte(private),
}

if err := req.Storage.Put(ctx, entry); err != nil {
return nil, err
}

decDoc[ej.PublicKeyField] = public

encDoc, err := EncryptEjsonDocument(ctx, decDoc)
if err != nil {
return nil, fmt.Errorf("failed to encrypt ejson")
}

return &logical.Response{
Data: map[string]interface{}{
"document": encDoc,
},
}, nil
}
106 changes: 106 additions & 0 deletions path_ejson_rotate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package secretsejson

import (
"context"
"encoding/json"
"reflect"
"testing"

ej "github.com/Shopify/ejson/json"
"github.com/hashicorp/vault/sdk/logical"
)

func TestEJSON_Keys_Rotate_InlineDoc(t *testing.T) {
b, storage := getTestBackend(t)
EJSON_Keys_Setup(t, b, storage)

publicKey := "15838c2f3260185ad2a8e1298bd507479ff2470b9e9c1fd89e0fdfefe2959f56"
dataInput := map[string]interface{}{
ej.PublicKeyField: publicKey,
"asecret": "EJ[1:sdseJpJ3BpP9PO5Qs8IB4urmmYil46edSTek8SjgVGA=:zl7mkBzL4g2d0PE3hPucmfbDjf3aDK7K:iryi3H7wRGWvUI8kjfWLtP3sFiw=]",
"_bsecret": "intentionally_left_unencrypted",
"anumber": 1,
}

reqRotate := &logical.Request{
Operation: logical.UpdateOperation,
Path: "rotate",
Storage: storage,
Data: dataInput,
}

respRead, err := b.HandleRequest(context.Background(), reqRotate)
if err != nil || (respRead != nil && respRead.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, respRead)
}

rotatedDoc, ok := respRead.Data["document"].(map[string]interface{})
if !ok {
t.Fatal("document missing from response", respRead)
}

if reflect.DeepEqual(rotatedDoc[ej.PublicKeyField], publicKey) {
t.Fatalf("public keys did not change: \nPublicKey: %#v\n", rotatedDoc[ej.PublicKeyField])
}

publicKeys, err := storage.List(context.Background(), "keys/")
if err != nil {
t.Fatal(err)
}

if len(publicKeys) != 2 {
t.Fatalf("keypairs for the rotated document missing from storage")
}
}

func TestEJSON_Keys_Rotate_ExplicitDoc(t *testing.T) {
b, storage := getTestBackend(t)
EJSON_Keys_Setup(t, b, storage)

publicKey := "15838c2f3260185ad2a8e1298bd507479ff2470b9e9c1fd89e0fdfefe2959f56"
ejsonDoc := map[string]interface{}{
ej.PublicKeyField: publicKey,
"asecret": "EJ[1:sdseJpJ3BpP9PO5Qs8IB4urmmYil46edSTek8SjgVGA=:zl7mkBzL4g2d0PE3hPucmfbDjf3aDK7K:iryi3H7wRGWvUI8kjfWLtP3sFiw=]",
"_bsecret": "intentionally_left_unencrypted",
"anumber": 1,
}

ejsonDocBytes, err := json.Marshal(ejsonDoc)
if err != nil {
t.Fatal(err)
}

dataInput := map[string]interface{}{
"document": string(ejsonDocBytes),
}

reqRotate := &logical.Request{
Operation: logical.UpdateOperation,
Path: "rotate",
Storage: storage,
Data: dataInput,
}

respRead, err := b.HandleRequest(context.Background(), reqRotate)
if err != nil || (respRead != nil && respRead.IsError()) {
t.Fatalf("err:%s resp:%#v\n", err, respRead)
}

rotatedDoc, ok := respRead.Data["document"].(map[string]interface{})
if !ok {
t.Fatal("document missing from response", respRead)
}

if reflect.DeepEqual(rotatedDoc[ej.PublicKeyField], publicKey) {
t.Fatalf("public keys did not change: \nPublicKey: %#v\n", rotatedDoc[ej.PublicKeyField])
}

publicKeys, err := storage.List(context.Background(), "keys/")
if err != nil {
t.Fatal(err)
}

if len(publicKeys) != 2 {
t.Fatalf("keypairs for the rotated document missing from storage")
}
}
10 changes: 0 additions & 10 deletions vendor/github.com/Shopify/ejson/.gitignore

This file was deleted.

11 changes: 0 additions & 11 deletions vendor/github.com/Shopify/ejson/.travis.yml

This file was deleted.

4 changes: 0 additions & 4 deletions vendor/github.com/Shopify/ejson/CHANGELOG.md

This file was deleted.

4 changes: 0 additions & 4 deletions vendor/github.com/Shopify/ejson/Gemfile

This file was deleted.

54 changes: 0 additions & 54 deletions vendor/github.com/Shopify/ejson/Gemfile.lock

This file was deleted.

Loading

0 comments on commit 31fc064

Please sign in to comment.