Skip to content

Commit

Permalink
add encrypt, decrypt and generate key APIs
Browse files Browse the repository at this point in the history
This commit adds the encrypt, decrypt and generate data encryption
key APIs.

Signed-off-by: Andreas Auernhammer <[email protected]>
  • Loading branch information
aead committed Dec 2, 2023
1 parent 0ab0dd7 commit 286d0a0
Show file tree
Hide file tree
Showing 10 changed files with 1,431 additions and 160 deletions.
163 changes: 152 additions & 11 deletions kms/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package kms

import (
"bytes"
"context"
"crypto/rand"
"crypto/tls"
Expand Down Expand Up @@ -149,16 +150,11 @@ func (c *Client) NodeStatus(ctx context.Context, endpoint string, _ *NodeStatusR
return nil, readError(resp)
}

var data pb.NodeStatusResponse
var data NodeStatusResponse
if err := readResponse(resp, &data); err != nil {
return nil, err
}

var stat NodeStatusResponse
if err := stat.UnmarshalPB(&data); err != nil {
return nil, err
}
return &stat, nil
return &data, nil
}

// Status returns status information about the entire KMS cluster. The
Expand Down Expand Up @@ -192,16 +188,161 @@ func (c *Client) Status(ctx context.Context, _ *StatusRequest) (*StatusResponse,
return nil, readError(resp)
}

var data pb.StatusResponse
var data StatusResponse
if err := readResponse(resp, &data); err != nil {
return nil, err
}
return &data, nil
}

// Encrypt encrypts a message with the key within the given enclave.
//
// It returns ErrEnclaveNotFound if no such enclave exists and
// ErrKeyNotFound if no such key exists.
func (c *Client) Encrypt(ctx context.Context, enclave, key string, req *EncryptRequest) (*EncryptResponse, error) {
const (
Method = http.MethodPut
Path = api.PathSecretKeyEncrypt
StatusOK = http.StatusOK
ContentType = headers.ContentTypeAppAny // accept JSON or protobuf
)

body, err := pb.Marshal(req)
if err != nil {
return nil, err
}

url, err := c.lb.URL(Path, key)
if err != nil {
return nil, err
}
r, err := http.NewRequestWithContext(ctx, Method, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
r.Header.Set(headers.Accept, ContentType)
r.Header.Set(headers.Enclave, enclave)

resp, err := c.client.Do(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != StatusOK {
return nil, readError(resp)
}

var data EncryptResponse
if err := readResponse(resp, &data); err != nil {
return nil, err
}
return &data, nil
}

// Decrypt decrypts a ciphertext with the key within the given enclave.
//
// It returns ErrEnclaveNotFound if no such enclave exists and
// ErrKeyNotFound if no such key exists.
func (c *Client) Decrypt(ctx context.Context, enclave, key string, req *DecryptRequest) (*DecryptResponse, error) {
const (
Method = http.MethodPut
Path = api.PathSecretKeyDecrypt
StatusOK = http.StatusOK
ContentType = headers.ContentTypeAppAny // accept JSON or protobuf
)

body, err := pb.Marshal(req)
if err != nil {
return nil, err
}

url, err := c.lb.URL(Path, key)
if err != nil {
return nil, err
}
r, err := http.NewRequestWithContext(ctx, Method, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
r.Header.Set(headers.Accept, ContentType)
r.Header.Set(headers.Enclave, enclave)

resp, err := c.client.Do(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != StatusOK {
return nil, readError(resp)
}

var data DecryptResponse
if err := readResponse(resp, &data); err != nil {
return nil, err
}
return &data, nil
}

var stat StatusResponse
if err := stat.UnmarshalPB(&data); err != nil {
// GenerateKey generates a new unique data encryption key. The returned
// GenerateKeyResponse contains a plaintext data encryption key with
// the requested length and a ciphertext version. The ciphertext
// is the plaintext data encryption key encrypted with the key within
// the given enclave.
//
// Applications should use, but never store, the plaintext data encryption
// key for cryptographic operations and remember the ciphertext version of
// the data encryption key. For example, encrypt a file with the plaintext
// data encryption key and store the ciphertext version of data encryption
// key alongside the encrypted file.
// The plaintext data encryption key can be obtained by decrypting the
// ciphertext data encryption key using Decrypt.
//
// Applications should also persist the key version that is used to prepare
// for future key rotation.
//
// It returns ErrEnclaveNotFound if no such enclave exists and
// ErrKeyNotFound if no such key exists.
func (c *Client) GenerateKey(ctx context.Context, enclave, key string, req *GenerateKeyRequest) (*GenerateKeyResponse, error) {
const (
Method = http.MethodPut
Path = api.PathSecretKeyGenerate
StatusOK = http.StatusOK
ContentType = headers.ContentTypeAppAny // accept JSON or protobuf
)

body, err := pb.Marshal(req)
if err != nil {
return nil, err
}

url, err := c.lb.URL(Path, key)
if err != nil {
return nil, err
}
r, err := http.NewRequestWithContext(ctx, Method, url, bytes.NewReader(body))
if err != nil {
return nil, err
}
r.Header.Set(headers.Accept, ContentType)
r.Header.Set(headers.Enclave, enclave)

resp, err := c.client.Do(r)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != StatusOK {
return nil, readError(resp)
}

var data GenerateKeyResponse
if err := readResponse(resp, &data); err != nil {
return nil, err
}
return &stat, nil
return &data, nil
}

// httpsURL turns the endpoint into an HTTPS endpoint.
Expand Down
8 changes: 8 additions & 0 deletions kms/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ var (
// an enclave that does not exist. For example, when trying to
// create a key in a non-existing enclave.
ErrEnclaveNotFound = Error{http.StatusNotFound, "enclave does not exist"}

// ErrKeyExists is returned when trying to create a key in an
// enclave that already contains a key with the same name.
ErrKeyExists = Error{http.StatusConflict, "key already exists"}

// ErrKeyNotFound is returned when trying to use a key that
// that does not exist.
ErrKeyNotFound = Error{http.StatusNotFound, "key does not exist"}
)

// Error is a KMS API error.
Expand Down
4 changes: 4 additions & 0 deletions kms/internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@ package api
const (
PathStatus = "/v1/status"

PathSecretKeyGenerate = "/v1/key/generate/"
PathSecretKeyEncrypt = "/v1/key/encrypt/"
PathSecretKeyDecrypt = "/v1/key/decrypt/"

PathClusterStatus = "/v1/cluster/status"
)
71 changes: 71 additions & 0 deletions kms/protobuf/proto.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,81 @@ package protobuf
import (
"time"

"google.golang.org/protobuf/proto"
pbd "google.golang.org/protobuf/types/known/durationpb"
pbt "google.golang.org/protobuf/types/known/timestamppb"
)

// Marshaler is an interface implemented by types that
// know how to marshal themselves into their protobuf
// representation T.
type Marshaler[T proto.Message] interface {
MarshalPB(T) error
}

// Unmarshaler is an interface implemented by types that
// know how to unmarshal themselves from their protobuf
// representation T.
type Unmarshaler[T proto.Message] interface {
UnmarshalPB(T) error
}

// Marshal returns v's protobuf binary data by first converting
// v into its protobuf representation type M and then marshaling
// M into the protobuf wire format.
func Marshal[M any, P Pointer[M], T Marshaler[P]](v T) ([]byte, error) {
var m M
if err := v.MarshalPB(&m); err != nil {
return nil, err
}

var p P = &m
return proto.Marshal(p)
}

// Unmarshal unmarshales v from b by first decoding b into v's
// protobuf representation M before converting M to v. It returns
// an error if b is not a valid protobuf representation of v.
func Unmarshal[M any, P Pointer[M], T Unmarshaler[P]](b []byte, v T) error {
var m M
var p P = &m
if err := proto.Unmarshal(b, p); err != nil {
return err
}
return v.UnmarshalPB(p)
}

// Pointer is a type constraint used to express that some
// type P is a pointer of some other type T such that:
//
// var t T
// var p P = &t
//
// This proposition is useful when unmarshaling data into types
// without additional dynamic dispatch or heap allocations.
//
// A generic function that wants to use the default value of
// some type T but also wants to call pointer receiver methods
// on instances of T has to have two type parameters:
//
// func foo[T any, P pointer[T]]() {
// var t T
// var p P = &t
// }
//
// This functionality cannot be achieved with a single type
// parameter because:
//
// func foo[T proto.Message]() {
// var t T // compiles but t is nil if T is a pointer type
// var t2 T = *new(T) // compiles but t2 is nil if T is a pointer type
// var t3 = T{} // compiler error - e.g. T may be a pointer type
// }
type Pointer[M any] interface {
proto.Message
*M // Anything implementing Pointer must also be a pointer type of M
}

// Time returns a new protobuf timestamp from the given t.
func Time(t time.Time) *pbt.Timestamp { return pbt.New(t) }

Expand Down
Loading

0 comments on commit 286d0a0

Please sign in to comment.