1
0
mirror of https://github.com/getsops/sops.git synced 2026-02-05 12:45:21 +01:00
Files
sops/kms/keysource.go
2019-01-24 16:50:41 +01:00

272 lines
8.0 KiB
Go

/*
Package kms contains an implementation of the go.mozilla.org/sops.MasterKey interface that encrypts and decrypts the
data key using AWS KMS with the AWS Go SDK.
*/
package kms //import "go.mozilla.org/sops/kms"
import (
"encoding/base64"
"fmt"
"os"
"regexp"
"strings"
"time"
"go.mozilla.org/sops/logging"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/kms"
"github.com/aws/aws-sdk-go/service/kms/kmsiface"
"github.com/aws/aws-sdk-go/service/sts"
"github.com/sirupsen/logrus"
)
var log *logrus.Logger
func init() {
log = logging.NewLogger("AWSKMS")
}
// this needs to be a global var for unit tests to work (mockKMS redefines
// it in keysource_test.go)
var kmsSvc kmsiface.KMSAPI
var isMocked bool
// MasterKey is a AWS KMS key used to encrypt and decrypt sops' data key.
type MasterKey struct {
Arn string
Role string
EncryptedKey string
CreationDate time.Time
EncryptionContext map[string]*string
}
// EncryptedDataKey returns the encrypted data key this master key holds
func (key *MasterKey) EncryptedDataKey() []byte {
return []byte(key.EncryptedKey)
}
// SetEncryptedDataKey sets the encrypted data key for this master key
func (key *MasterKey) SetEncryptedDataKey(enc []byte) {
key.EncryptedKey = string(enc)
}
// Encrypt takes a sops data key, encrypts it with KMS and stores the result in the EncryptedKey field
func (key *MasterKey) Encrypt(dataKey []byte) error {
// isMocked is set by unit test to indicate that the KMS service
// has already been initialized. it's ugly, but it works.
if kmsSvc == nil || !isMocked {
sess, err := key.createSession()
if err != nil {
log.WithField("arn", key.Arn).Info("Encryption failed")
return fmt.Errorf("Failed to create session: %v", err)
}
kmsSvc = kms.New(sess)
}
out, err := kmsSvc.Encrypt(&kms.EncryptInput{Plaintext: dataKey, KeyId: &key.Arn, EncryptionContext: key.EncryptionContext})
if err != nil {
log.WithField("arn", key.Arn).Info("Encryption failed")
return fmt.Errorf("Failed to call KMS encryption service: %v", err)
}
key.EncryptedKey = base64.StdEncoding.EncodeToString(out.CiphertextBlob)
log.WithField("arn", key.Arn).Info("Encryption succeeded")
return nil
}
// EncryptIfNeeded encrypts the provided sops' data key and encrypts it if it hasn't been encrypted yet
func (key *MasterKey) EncryptIfNeeded(dataKey []byte) error {
if key.EncryptedKey == "" {
return key.Encrypt(dataKey)
}
return nil
}
// Decrypt decrypts the EncryptedKey field with AWS KMS and returns the result.
func (key *MasterKey) Decrypt() ([]byte, error) {
k, err := base64.StdEncoding.DecodeString(key.EncryptedKey)
if err != nil {
log.WithField("arn", key.Arn).Info("Decryption failed")
return nil, fmt.Errorf("Error base64-decoding encrypted data key: %s", err)
}
// isMocked is set by unit test to indicate that the KMS service
// has already been initialized. it's ugly, but it works.
if kmsSvc == nil || !isMocked {
sess, err := key.createSession()
if err != nil {
log.WithField("arn", key.Arn).Info("Decryption failed")
return nil, fmt.Errorf("Error creating AWS session: %v", err)
}
kmsSvc = kms.New(sess)
}
decrypted, err := kmsSvc.Decrypt(&kms.DecryptInput{CiphertextBlob: k, EncryptionContext: key.EncryptionContext})
if err != nil {
log.WithField("arn", key.Arn).Info("Decryption failed")
return nil, fmt.Errorf("Error decrypting key: %v", err)
}
log.WithField("arn", key.Arn).Info("Decryption succeeded")
return decrypted.Plaintext, nil
}
// NeedsRotation returns whether the data key needs to be rotated or not.
func (key *MasterKey) NeedsRotation() bool {
return time.Since(key.CreationDate) > (time.Hour * 24 * 30 * 6)
}
// ToString converts the key to a string representation
func (key *MasterKey) ToString() string {
return key.Arn
}
// NewMasterKey creates a new MasterKey from an ARN, role and context, setting the creation date to the current date
func NewMasterKey(arn string, role string, context map[string]*string) *MasterKey {
return &MasterKey{
Arn: arn,
Role: role,
EncryptionContext: context,
CreationDate: time.Now().UTC(),
}
}
// NewMasterKeyFromArn takes an ARN string and returns a new MasterKey for that ARN
func NewMasterKeyFromArn(arn string, context map[string]*string) *MasterKey {
k := &MasterKey{}
arn = strings.Replace(arn, " ", "", -1)
roleIndex := strings.Index(arn, "+arn:aws:iam::")
if roleIndex > 0 {
k.Arn = arn[:roleIndex]
k.Role = arn[roleIndex+1:]
} else {
k.Arn = arn
}
k.EncryptionContext = context
k.CreationDate = time.Now().UTC()
return k
}
// MasterKeysFromArnString takes a comma separated list of AWS KMS ARNs and returns a slice of new MasterKeys for those ARNs
func MasterKeysFromArnString(arn string, context map[string]*string) []*MasterKey {
var keys []*MasterKey
if arn == "" {
return keys
}
for _, s := range strings.Split(arn, ",") {
keys = append(keys, NewMasterKeyFromArn(s, context))
}
return keys
}
func (key MasterKey) createStsSession(config aws.Config, sess *session.Session) (*session.Session, error) {
hostname, err := os.Hostname()
if err != nil {
return nil, err
}
stsService := sts.New(sess)
name := "sops@" + hostname
out, err := stsService.AssumeRole(&sts.AssumeRoleInput{
RoleArn: &key.Role, RoleSessionName: &name})
if err != nil {
return nil, fmt.Errorf("Failed to assume role %q: %v", key.Role, err)
}
config.Credentials = credentials.NewStaticCredentials(*out.Credentials.AccessKeyId,
*out.Credentials.SecretAccessKey, *out.Credentials.SessionToken)
sess, err = session.NewSession(&config)
if err != nil {
return nil, fmt.Errorf("Failed to create new aws session: %v", err)
}
return sess, nil
}
func (key MasterKey) createSession() (*session.Session, error) {
re := regexp.MustCompile(`^arn:aws[\w-]*:kms:(.+):[0-9]+:(key|alias)/.+$`)
matches := re.FindStringSubmatch(key.Arn)
if matches == nil {
return nil, fmt.Errorf("No valid ARN found in %q", key.Arn)
}
config := aws.Config{Region: aws.String(matches[1])}
opts := session.Options{
Config: config,
AssumeRoleTokenProvider: stscreds.StdinTokenProvider,
}
sess, err := session.NewSessionWithOptions(opts)
if err != nil {
return nil, err
}
if key.Role != "" {
return key.createStsSession(config, sess)
}
return sess, nil
}
// ToMap converts the MasterKey to a map for serialization purposes
func (key MasterKey) ToMap() map[string]interface{} {
out := make(map[string]interface{})
out["arn"] = key.Arn
if key.Role != "" {
out["role"] = key.Role
}
out["created_at"] = key.CreationDate.UTC().Format(time.RFC3339)
out["enc"] = key.EncryptedKey
if key.EncryptionContext != nil {
outcontext := make(map[string]string)
for k, v := range key.EncryptionContext {
outcontext[k] = *v
}
out["context"] = outcontext
}
return out
}
// ParseKMSContext takes either a KMS context map or a comma-separated list of KMS context key:value pairs and returns a map
func ParseKMSContext(in interface{}) map[string]*string {
nonStringValueWarning := "Encryption context contains a non-string value, context will not be used"
out := make(map[string]*string)
switch in := in.(type) {
case map[string]interface{}:
if len(in) == 0 {
return nil
}
for k, v := range in {
value, ok := v.(string)
if !ok {
log.Warn(nonStringValueWarning)
return nil
}
out[k] = &value
}
case map[interface{}]interface{}:
if len(in) == 0 {
return nil
}
for k, v := range in {
key, ok := k.(string)
if !ok {
log.Warn(nonStringValueWarning)
return nil
}
value, ok := v.(string)
if !ok {
log.Warn(nonStringValueWarning)
return nil
}
out[key] = &value
}
case string:
if in == "" {
return nil
}
for _, kv := range strings.Split(in, ",") {
kv := strings.Split(kv, ":")
if len(kv) != 2 {
log.Warn(nonStringValueWarning)
return nil
}
out[kv[0]] = &kv[1]
}
}
return out
}