Skip to content

Commit

Permalink
Merge pull request #2386 from replicatedhq/laverya/centralize-encrypt…
Browse files Browse the repository at this point in the history
…ion-key-management

centralize encryption key management
  • Loading branch information
laverya committed Dec 9, 2021
2 parents 10b6d37 + 675288d commit 0404926
Show file tree
Hide file tree
Showing 34 changed files with 209 additions and 255 deletions.
20 changes: 10 additions & 10 deletions kotskinds/apis/kots/v1beta1/identityconfig_types.go
Expand Up @@ -47,13 +47,13 @@ type StringValueOrEncrypted struct {
ValueEncrypted string `json:"valueEncrypted,omitempty" yaml:"valueEncrypted,omitempty"`
}

func NewStringValueOrEncrypted(value string, cipher crypto.AESCipher) *StringValueOrEncrypted {
func NewStringValueOrEncrypted(value string) *StringValueOrEncrypted {
v := &StringValueOrEncrypted{Value: value}
v.EncryptValue(cipher)
v.EncryptValue()
return v
}

func (v *StringValueOrEncrypted) GetValue(cipher crypto.AESCipher) (string, error) {
func (v *StringValueOrEncrypted) GetValue() (string, error) {
if v == nil {
return "", nil
}
Expand All @@ -62,17 +62,17 @@ func (v *StringValueOrEncrypted) GetValue(cipher crypto.AESCipher) (string, erro
if err != nil {
return "", errors.Wrap(err, "failed to base64 decode")
}
result, err := cipher.Decrypt(b)
result, err := crypto.Decrypt(b)
return string(result), errors.Wrap(err, "failed to decrypt")
}
return v.Value, nil
}

func (v *StringValueOrEncrypted) EncryptValue(cipher crypto.AESCipher) {
func (v *StringValueOrEncrypted) EncryptValue() {
if v.ValueEncrypted != "" && v.Value == "" {
return
}
v.ValueEncrypted = base64.StdEncoding.EncodeToString(cipher.Encrypt([]byte(v.Value)))
v.ValueEncrypted = base64.StdEncoding.EncodeToString(crypto.Encrypt([]byte(v.Value)))
v.Value = ""
}

Expand All @@ -99,13 +99,13 @@ type DexConnectors struct {
ValueFrom *DexConnectorsSource `json:"valueFrom,omitempty" yaml:"valueFrom,omitempty"`
}

func (v *DexConnectors) GetValue(cipher crypto.AESCipher) ([]DexConnector, error) {
func (v *DexConnectors) GetValue() ([]DexConnector, error) {
if v.ValueEncrypted != "" {
b, err := base64.StdEncoding.DecodeString(v.ValueEncrypted)
if err != nil {
return nil, errors.Wrap(err, "failed to base64 decode")
}
result, err := cipher.Decrypt(b)
result, err := crypto.Decrypt(b)
if err != nil {
return nil, errors.Wrap(err, "failed to decrypt")
}
Expand All @@ -118,7 +118,7 @@ func (v *DexConnectors) GetValue(cipher crypto.AESCipher) ([]DexConnector, error
return v.Value, nil
}

func (v *DexConnectors) EncryptValue(cipher crypto.AESCipher) error {
func (v *DexConnectors) EncryptValue() error {
if v.ValueEncrypted != "" && len(v.Value) == 0 {
return nil
}
Expand All @@ -127,7 +127,7 @@ func (v *DexConnectors) EncryptValue(cipher crypto.AESCipher) error {
if err != nil {
return err
}
v.ValueEncrypted = base64.StdEncoding.EncodeToString(cipher.Encrypt(b))
v.ValueEncrypted = base64.StdEncoding.EncodeToString(crypto.Encrypt(b))
v.Value = nil
return nil
}
Expand Down
3 changes: 1 addition & 2 deletions pkg/airgap/airgap.go
Expand Up @@ -16,7 +16,6 @@ import (
kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
"github.com/replicatedhq/kots/pkg/airgap/types"
"github.com/replicatedhq/kots/pkg/archives"
"github.com/replicatedhq/kots/pkg/crypto"
kotsadmconfig "github.com/replicatedhq/kots/pkg/kotsadmconfig"
identity "github.com/replicatedhq/kots/pkg/kotsadmidentity"
"github.com/replicatedhq/kots/pkg/kotsutil"
Expand Down Expand Up @@ -180,7 +179,7 @@ func CreateAppFromAirgap(opts CreateAirgapAppOpts) (finalError error) {
configFile = tmpFile.Name()
}

identityConfigFile, err := identity.InitAppIdentityConfig(opts.PendingApp.Slug, kotsv1beta1.Storage{}, crypto.AESCipher{})
identityConfigFile, err := identity.InitAppIdentityConfig(opts.PendingApp.Slug, kotsv1beta1.Storage{})
if err != nil {
return errors.Wrap(err, "failed to init identity config")
}
Expand Down
3 changes: 1 addition & 2 deletions pkg/airgap/update.go
Expand Up @@ -12,7 +12,6 @@ import (
"github.com/pkg/errors"
kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
apptypes "github.com/replicatedhq/kots/pkg/app/types"
"github.com/replicatedhq/kots/pkg/crypto"
"github.com/replicatedhq/kots/pkg/cursor"
identity "github.com/replicatedhq/kots/pkg/kotsadmidentity"
"github.com/replicatedhq/kots/pkg/kotsutil"
Expand Down Expand Up @@ -142,7 +141,7 @@ func UpdateAppFromPath(a *apptypes.App, airgapRoot string, airgapBundlePath stri

identityConfigFile := filepath.Join(archiveDir, "upstream", "userdata", "identityconfig.yaml")
if _, err := os.Stat(identityConfigFile); os.IsNotExist(err) {
file, err := identity.InitAppIdentityConfig(a.Slug, kotsv1beta1.Storage{}, crypto.AESCipher{})
file, err := identity.InitAppIdentityConfig(a.Slug, kotsv1beta1.Storage{})
if err != nil {
return errors.Wrap(err, "failed to init identity config")
}
Expand Down
14 changes: 1 addition & 13 deletions pkg/apiserver/bootstrap.go
Expand Up @@ -7,10 +7,8 @@ import (

"github.com/pkg/errors"
kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
"github.com/replicatedhq/kots/pkg/crypto"
identity "github.com/replicatedhq/kots/pkg/kotsadmidentity"
identitystore "github.com/replicatedhq/kots/pkg/kotsadmidentity/store"
"github.com/replicatedhq/kots/pkg/kotsutil"
"github.com/replicatedhq/kots/pkg/store"
"k8s.io/client-go/kubernetes/scheme"
)
Expand Down Expand Up @@ -113,17 +111,7 @@ func bootstrapIdentity() error {
}
identityConfig := obj.(*kotsv1beta1.IdentityConfig)

installation, err := kotsutil.LoadInstallationFromPath(filepath.Join(currentArchivePath, "upstream", "userdata", "installation.yaml"))
if err != nil {
return errors.Wrap(err, "failed to load installation from path")
}

apiCipher, err := crypto.AESCipherFromString(installation.Spec.EncryptionKey)
if err != nil {
return errors.Wrap(err, "failed to create aes cipher")
}

identityConfigFile, err = identity.InitAppIdentityConfig(app.Slug, identityConfig.Spec.Storage, *apiCipher)
identityConfigFile, err = identity.InitAppIdentityConfig(app.Slug, identityConfig.Spec.Storage)
if err != nil {
return errors.Wrap(err, "failed to init identity config")
}
Expand Down
11 changes: 0 additions & 11 deletions pkg/base/templates.go
Expand Up @@ -3,7 +3,6 @@ package base
import (
"github.com/pkg/errors"
kotsv1beta1 "github.com/replicatedhq/kots/kotskinds/apis/kots/v1beta1"
"github.com/replicatedhq/kots/pkg/crypto"
"github.com/replicatedhq/kots/pkg/template"
upstreamtypes "github.com/replicatedhq/kots/pkg/upstream/types"
)
Expand All @@ -30,15 +29,6 @@ func NewConfigContextTemplateBuilder(u *upstreamtypes.Upstream, renderOptions *R
templateContext = map[string]template.ItemValue{}
}

var cipher *crypto.AESCipher
if u.EncryptionKey != "" {
c, err := crypto.AESCipherFromString(u.EncryptionKey)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to create cipher")
}
cipher = c
}

configGroups := []kotsv1beta1.ConfigGroup{}
if kotsKinds.Config != nil {
configGroups = kotsKinds.Config.Spec.Groups
Expand Down Expand Up @@ -69,7 +59,6 @@ func NewConfigContextTemplateBuilder(u *upstreamtypes.Upstream, renderOptions *R
ConfigGroups: configGroups,
ExistingValues: templateContext,
LocalRegistry: localRegistry,
Cipher: cipher,
License: kotsKinds.License,
Application: &kotsKinds.KotsApplication,
VersionInfo: &versionInfo,
Expand Down
1 change: 0 additions & 1 deletion pkg/config/config.go
Expand Up @@ -47,7 +47,6 @@ func templateConfigObjects(configSpec *kotsv1beta1.Config, configValues map[stri
ConfigGroups: configSpec.Spec.Groups,
ExistingValues: configValues,
LocalRegistry: localRegistry,
Cipher: nil,
License: license,
Application: app,
VersionInfo: versionInfo,
Expand Down
138 changes: 120 additions & 18 deletions pkg/crypto/aes.go
@@ -1,51 +1,134 @@
package crypto

import (
"bytes"
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"os"

"github.com/pkg/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)

type AESCipher struct {
type aesCipher struct {
key []byte
cipher cipher.AEAD
nonce []byte
}

const keyLength = 24 // 192 bit

func NewAESCipher() (*AESCipher, error) {
var decryptionCiphers []*aesCipher // used to decrypt data
var encryptionCipher *aesCipher // used to encrypt data

// add cipher from API_ENCRYPTION_KEY environment variable if it is present (and set that key to be used for encryption)
func init() {
decryptionCiphers = []*aesCipher{}
if os.Getenv("API_ENCRYPTION_KEY") != "" {
envCipher, err := aesCipherFromString(os.Getenv("API_ENCRYPTION_KEY"))
if err != nil {
// do nothing - the secret can still be initialized from a different source
} else {
decryptionCiphers = append(decryptionCiphers, envCipher)
encryptionCipher = envCipher
}
}
}

// InitFromSecret reads the encryption key from kubernetes and adds it to the list of decryptionCiphers, and sets this key to be used for encryption.
func InitFromSecret(clientset kubernetes.Interface, namespace string) error {
sec, err := clientset.CoreV1().Secrets(namespace).Get(context.Background(), "kotsadm-encryption", metav1.GetOptions{})
if err != nil {
return errors.Wrap(err, "get kotsadm-encryption secret")
}

secData, ok := sec.Data["encryptionKey"]
if !ok {
return fmt.Errorf("kotsadm-encryption secret in %s does not have member encryptionKey", namespace)
}

secCipher, err := aesCipherFromString(string(secData))
if err != nil {
return errors.Wrap(err, "parse kotsadm-encryption secret")
}

addCipher(secCipher)
encryptionCipher = secCipher

return nil
}

// InitFromString parses the encryption key from the provided string and adds it to the list of decryptionCiphers
func InitFromString(data string) error {
if data == "" {
return nil
}

newCipher, err := aesCipherFromString(data)
if err != nil {
return err
}
addCipher(newCipher)
return nil
}

// check if a cipher exists in the array, if it does not then add it
func addCipher(aesCipher *aesCipher) {
foundMatch := false
for _, existingCipher := range decryptionCiphers {
if bytes.Compare(existingCipher.key, aesCipher.key) == 0 && bytes.Compare(existingCipher.nonce, aesCipher.nonce) == 0 {
foundMatch = true
}
}

if !foundMatch {
decryptionCiphers = append(decryptionCiphers, aesCipher)
}
}

// NewAESCipher creates a new AES cipher to be used for encryption and decryption. If one already exists, it is used instead.
func NewAESCipher() error {
if encryptionCipher != nil && len(decryptionCiphers) >= 1 {
return nil
}

key := make([]byte, keyLength)
if _, err := rand.Read(key); err != nil {
return nil, errors.Wrap(err, "failed to read key")
return errors.Wrap(err, "failed to read key")
}

block, err := aes.NewCipher(key)
if err != nil {
return nil, errors.Wrap(err, "failed ro create new cipher")
return errors.Wrap(err, "failed to create new cipher")
}

gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, errors.Wrap(err, "failed to wrap cipher gcm")
return errors.Wrap(err, "failed to wrap cipher gcm")
}

nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, errors.Wrap(err, "failed to read nonce")
return errors.Wrap(err, "failed to read nonce")
}

return &AESCipher{
newCipher := &aesCipher{
key: key,
cipher: gcm,
nonce: nonce,
}, nil
}

addCipher(newCipher)
encryptionCipher = newCipher
return nil
}

func AESCipherFromString(data string) (aesCipher *AESCipher, initErr error) {
func aesCipherFromString(data string) (newCipher *aesCipher, initErr error) {
defer func() {
if r := recover(); r != nil {
initErr = errors.Errorf("cipher init recovered from panic: %v", r)
Expand Down Expand Up @@ -82,7 +165,7 @@ func AESCipherFromString(data string) (aesCipher *AESCipher, initErr error) {
return
}

aesCipher = &AESCipher{
newCipher = &aesCipher{
key: key,
cipher: gcm,
nonce: decoded[keyLength:],
Expand All @@ -91,18 +174,15 @@ func AESCipherFromString(data string) (aesCipher *AESCipher, initErr error) {
return
}

func (c *AESCipher) ToString() string {
if c == nil {
// ToString returns a string representation of the global encryption key
func ToString() string {
if encryptionCipher == nil {
return ""
}
return base64.StdEncoding.EncodeToString(append(c.key, c.nonce...))
return base64.StdEncoding.EncodeToString(append(encryptionCipher.key, encryptionCipher.nonce...))
}

func (c *AESCipher) Encrypt(in []byte) []byte {
return c.cipher.Seal(nil, c.nonce, in, nil)
}

func (c *AESCipher) Decrypt(in []byte) (result []byte, err error) {
func (c *aesCipher) decrypt(in []byte) (result []byte, err error) {
defer func() {
if r := recover(); r != nil {
err = errors.Errorf("decrypt recovered from panic: %v", r)
Expand All @@ -112,3 +192,25 @@ func (c *AESCipher) Decrypt(in []byte) (result []byte, err error) {
result, err = c.cipher.Open(nil, c.nonce, in, nil)
return
}

// Encrypt encrypts the data with the registered encryption key
func Encrypt(in []byte) []byte {
if encryptionCipher == nil {
_ = NewAESCipher()
}

return encryptionCipher.cipher.Seal(nil, encryptionCipher.nonce, in, nil)
}

// Decrypt attempts to decrypt the provided data with all registered keys
func Decrypt(in []byte) (result []byte, err error) {
for _, decryptCipher := range decryptionCiphers {
result, err = decryptCipher.decrypt(in)
if err != nil {
continue
} else {
return result, nil
}
}
return nil, err
}

0 comments on commit 0404926

Please sign in to comment.