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.
This commit is contained in:
Jeff Mitchell 2015-09-18 09:50:53 -04:00
parent 61398f1b01
commit 448249108c
3 changed files with 210 additions and 0 deletions

View file

@ -27,6 +27,7 @@ func Backend() *framework.Backend {
pathKeys(),
pathEncrypt(),
pathDecrypt(),
pathDatakey(),
},
Secrets: []*framework.Secret{},

View file

@ -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",

View file

@ -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.
`