From 448249108c0ec7c8de7a4fc6b60b2063ac83d607 Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Fri, 18 Sep 2015 09:50:53 -0400 Subject: [PATCH] Add datakey generation to transit. Can specify 128 bits (defaults to 256) and control whether or not plaintext is returned (default true). Unit tests for all of the new functionality. --- builtin/logical/transit/backend.go | 1 + builtin/logical/transit/backend_test.go | 80 +++++++++++++++ builtin/logical/transit/path_datakey.go | 129 ++++++++++++++++++++++++ 3 files changed, 210 insertions(+) create mode 100644 builtin/logical/transit/path_datakey.go diff --git a/builtin/logical/transit/backend.go b/builtin/logical/transit/backend.go index 1510fbaff..9ff64896c 100644 --- a/builtin/logical/transit/backend.go +++ b/builtin/logical/transit/backend.go @@ -27,6 +27,7 @@ func Backend() *framework.Backend { pathKeys(), pathEncrypt(), pathDecrypt(), + pathDatakey(), }, Secrets: []*framework.Secret{}, diff --git a/builtin/logical/transit/backend_test.go b/builtin/logical/transit/backend_test.go index ada1fc61f..c43eac387 100644 --- a/builtin/logical/transit/backend_test.go +++ b/builtin/logical/transit/backend_test.go @@ -39,6 +39,20 @@ func TestBackend_basic(t *testing.T) { }) } +func TestBackend_datakey(t *testing.T) { + dataKeyInfo := make(map[string]interface{}) + logicaltest.Test(t, logicaltest.TestCase{ + Backend: Backend(), + Steps: []logicaltest.TestStep{ + testAccStepWritePolicy(t, "test", false), + testAccStepReadPolicy(t, "test", false, false), + testAccStepWriteDatakey(t, "test", false, 256, dataKeyInfo), + testAccStepDecryptDatakey(t, "test", dataKeyInfo), + testAccStepWriteDatakey(t, "test", true, 128, dataKeyInfo), + }, + }) +} + func TestBackend_rotation(t *testing.T) { decryptData := make(map[string]interface{}) encryptHistory := make(map[int]map[string]interface{}) @@ -423,6 +437,72 @@ func testAccStepRotate(t *testing.T, name string) logicaltest.TestStep { } } +func testAccStepWriteDatakey(t *testing.T, name string, + noPlaintext bool, bits int, + dataKeyInfo map[string]interface{}) logicaltest.TestStep { + data := map[string]interface{}{} + if noPlaintext { + data["no_plaintext"] = true + } + if bits != 256 { + data["bits"] = bits + } + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "datakey/" + name, + Data: data, + Check: func(resp *logical.Response) error { + var d struct { + Plaintext string `mapstructure:"plaintext"` + Ciphertext string `mapstructure:"ciphertext"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + if noPlaintext && len(d.Plaintext) != 0 { + return fmt.Errorf("received plaintxt when we disabled it") + } + if !noPlaintext { + if len(d.Plaintext) == 0 { + return fmt.Errorf("did not get plaintext when we expected it") + } + dataKeyInfo["plaintext"] = d.Plaintext + plainBytes, err := base64.StdEncoding.DecodeString(d.Plaintext) + if err != nil { + return fmt.Errorf("could not base64 decode plaintext string '%s'", d.Plaintext) + } + if len(plainBytes)*8 != bits { + return fmt.Errorf("returned key does not have correct bit length") + } + } + dataKeyInfo["ciphertext"] = d.Ciphertext + return nil + }, + } +} + +func testAccStepDecryptDatakey(t *testing.T, name string, + dataKeyInfo map[string]interface{}) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "decrypt/" + name, + Data: dataKeyInfo, + Check: func(resp *logical.Response) error { + var d struct { + Plaintext string `mapstructure:"plaintext"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + + if d.Plaintext != dataKeyInfo["plaintext"].(string) { + return fmt.Errorf("plaintext mismatch: got '%s', expected '%s', decryptData was %#v", d.Plaintext, dataKeyInfo["plaintext"].(string)) + } + return nil + }, + } +} + func TestKeyUpgrade(t *testing.T) { p := &Policy{ Name: "test", diff --git a/builtin/logical/transit/path_datakey.go b/builtin/logical/transit/path_datakey.go new file mode 100644 index 000000000..28715415c --- /dev/null +++ b/builtin/logical/transit/path_datakey.go @@ -0,0 +1,129 @@ +package transit + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + + "github.com/hashicorp/vault/helper/certutil" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathDatakey() *framework.Path { + return &framework.Path{ + Pattern: "datakey/" + framework.GenericNameRegex("name"), + Fields: map[string]*framework.FieldSchema{ + "name": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The backend key used for encrypting the data key", + }, + + "context": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Context for key derivation. Required for derived keys.", + }, + + "bits": &framework.FieldSchema{ + Type: framework.TypeInt, + Description: `Number of bits for the key; currently 128 and +256 are supported. Defaults to 256.`, + Default: 256, + }, + + "no_plaintext": &framework.FieldSchema{ + Type: framework.TypeBool, + Description: "If set, the plaintext of the key will not be returned", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: pathDatakeyWrite, + }, + + HelpSynopsis: pathDatakeyHelpSyn, + HelpDescription: pathDatakeyHelpDesc, + } +} + +func pathDatakeyWrite( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + name := d.Get("name").(string) + + // Decode the context if any + contextRaw := d.Get("context").(string) + var context []byte + if len(contextRaw) != 0 { + var err error + context, err = base64.StdEncoding.DecodeString(contextRaw) + if err != nil { + return logical.ErrorResponse("failed to decode context as base64"), logical.ErrInvalidRequest + } + } + + // Get the policy + p, err := getPolicy(req, name) + if err != nil { + return nil, err + } + + // Error if invalid policy + if p == nil { + return logical.ErrorResponse("policy not found"), logical.ErrInvalidRequest + } + + newKey := make([]byte, 32) + bits := d.Get("bits").(int) + switch bits { + case 256: + case 128: + newKey = make([]byte, 16) + default: + return logical.ErrorResponse("invalid bit length"), logical.ErrInvalidRequest + } + _, err = rand.Read(newKey) + if err != nil { + return nil, err + } + + ciphertext, err := p.Encrypt(context, base64.StdEncoding.EncodeToString(newKey)) + if err != nil { + switch err.(type) { + case certutil.UserError: + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + case certutil.InternalError: + return nil, err + default: + return nil, err + } + } + + if ciphertext == "" { + return nil, fmt.Errorf("empty ciphertext returned") + } + + // Generate the response + resp := &logical.Response{ + Data: map[string]interface{}{ + "ciphertext": ciphertext, + }, + } + + if !d.Get("no_plaintext").(bool) { + resp.Data["plaintext"] = base64.StdEncoding.EncodeToString(newKey) + } + + return resp, nil +} + +const pathDatakeyHelpSyn = `Generate a data key` + +const pathDatakeyHelpDesc = ` +This path can be used to generate a data key: a random +key of a certain length that can be used for encryption +and decryption, protected by the named backend key. 128 +or 256 bits can be specified; if not specified, the default +is 256 bits. The "no_plaintext" parameter can be used to +prevent the (base64-encoded) plaintext key from being +returned along with the encrypted key. +`