pulumi/sdk/go/common/resource/config/crypt.go

267 lines
8.5 KiB
Go

// Copyright 2016-2022, Pulumi Corporation.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"context"
"crypto/aes"
"crypto/cipher"
cryptorand "crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"strings"
"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
"golang.org/x/crypto/pbkdf2"
)
// Encrypter encrypts plaintext into its encrypted ciphertext.
type Encrypter interface {
EncryptValue(ctx context.Context, plaintext string) (string, error)
}
// Decrypter decrypts encrypted ciphertext to its plaintext representation.
type Decrypter interface {
DecryptValue(ctx context.Context, ciphertext string) (string, error)
// BulkDecrypt supports bulk decryption of secrets.
BulkDecrypt(ctx context.Context, ciphertexts []string) (map[string]string, error)
}
// Crypter can both encrypt and decrypt values.
type Crypter interface {
Encrypter
Decrypter
}
// A nopCrypter simply returns the ciphertext as-is.
type nopCrypter struct{}
var (
NopDecrypter Decrypter = nopCrypter{}
NopEncrypter Encrypter = nopCrypter{}
)
func (nopCrypter) DecryptValue(ctx context.Context, ciphertext string) (string, error) {
return ciphertext, nil
}
func (nopCrypter) BulkDecrypt(ctx context.Context, ciphertexts []string) (map[string]string, error) {
return DefaultBulkDecrypt(ctx, NopDecrypter, ciphertexts)
}
func (nopCrypter) EncryptValue(ctx context.Context, plaintext string) (string, error) {
return plaintext, nil
}
// BlindingCrypter returns a Crypter that instead of decrypting or encrypting data, just returns "[secret]", it can
// be used when you want to display configuration information to a user but don't want to prompt for a password
// so secrets will not be decrypted or encrypted.
var BlindingCrypter Crypter = blindingCrypter{}
// NewBlindingDecrypter returns a blinding decrypter.
func NewBlindingDecrypter() Decrypter {
return blindingCrypter{}
}
type blindingCrypter struct{}
func (b blindingCrypter) DecryptValue(ctx context.Context, _ string) (string, error) {
return "[secret]", nil
}
func (b blindingCrypter) EncryptValue(ctx context.Context, plaintext string) (string, error) {
return "[secret]", nil
}
func (b blindingCrypter) BulkDecrypt(ctx context.Context, ciphertexts []string) (map[string]string, error) {
return DefaultBulkDecrypt(ctx, b, ciphertexts)
}
// NewPanicCrypter returns a new config crypter that will panic if used.
func NewPanicCrypter() Crypter {
return &panicCrypter{}
}
type panicCrypter struct{}
func (p panicCrypter) EncryptValue(ctx context.Context, _ string) (string, error) {
panic("attempt to encrypt value")
}
func (p panicCrypter) DecryptValue(ctx context.Context, _ string) (string, error) {
panic("attempt to decrypt value")
}
func (p panicCrypter) BulkDecrypt(ctx context.Context, ciphertexts []string) (map[string]string, error) {
panic("attempt to bulk decrypt values")
}
// NewSymmetricCrypter creates a crypter that encrypts and decrypts values using AES-256-GCM. The nonce is stored with
// the value itself as a pair of base64 values separated by a colon and a version tag `v1` is prepended.
func NewSymmetricCrypter(key []byte) Crypter {
contract.Requiref(len(key) == SymmetricCrypterKeyBytes, "key", "AES-256-GCM needs a 32 byte key")
return &symmetricCrypter{key}
}
// NewSymmetricCrypterFromPassphrase uses a passphrase and salt to generate a key, and then returns a crypter using it.
func NewSymmetricCrypterFromPassphrase(phrase string, salt []byte) Crypter {
// Generate a key using PBKDF2 to slow down attempts to crack it. 1,000,000 iterations was chosen because it
// took a little over a second on an i7-7700HQ Quad Core processor
key := pbkdf2.Key([]byte(phrase), salt, 1000000, SymmetricCrypterKeyBytes, sha256.New)
return NewSymmetricCrypter(key)
}
// SymmetricCrypterKeyBytes is the required key size in bytes.
const SymmetricCrypterKeyBytes = 32
type symmetricCrypter struct {
key []byte
}
func (s symmetricCrypter) EncryptValue(ctx context.Context, value string) (string, error) {
secret, nonce := encryptAES256GCGM(value, s.key)
return fmt.Sprintf("v1:%s:%s",
base64.StdEncoding.EncodeToString(nonce), base64.StdEncoding.EncodeToString(secret)), nil
}
func (s symmetricCrypter) DecryptValue(ctx context.Context, value string) (string, error) {
vals := strings.Split(value, ":")
if len(vals) != 3 {
return "", errors.New("bad value")
}
if vals[0] != "v1" {
return "", errors.New("unknown value version")
}
nonce, err := base64.StdEncoding.DecodeString(vals[1])
if err != nil {
return "", fmt.Errorf("bad value: %w", err)
}
enc, err := base64.StdEncoding.DecodeString(vals[2])
if err != nil {
return "", fmt.Errorf("bad value: %w", err)
}
return decryptAES256GCM(enc, s.key, nonce)
}
func (s symmetricCrypter) BulkDecrypt(ctx context.Context, ciphertexts []string) (map[string]string, error) {
return DefaultBulkDecrypt(ctx, s, ciphertexts)
}
// encryptAES256GCGM returns the ciphertext and the generated nonce
func encryptAES256GCGM(plaintext string, key []byte) ([]byte, []byte) {
contract.Requiref(len(key) == SymmetricCrypterKeyBytes, "key", "AES-256-GCM needs a 32 byte key")
nonce := make([]byte, 12)
_, err := cryptorand.Read(nonce)
contract.Assertf(err == nil, "could not read from system random source")
block, err := aes.NewCipher(key)
contract.AssertNoErrorf(err, "error creating AES cipher")
aesgcm, err := cipher.NewGCM(block)
contract.AssertNoErrorf(err, "error creating AES-GCM cipher")
msg := aesgcm.Seal(nil, nonce, []byte(plaintext), nil)
return msg, nonce
}
func decryptAES256GCM(ciphertext []byte, key []byte, nonce []byte) (string, error) {
contract.Requiref(len(key) == SymmetricCrypterKeyBytes, "key", "AES-256-GCM needs a 32 byte key")
block, err := aes.NewCipher(key)
contract.AssertNoErrorf(err, "error creating AES cipher")
aesgcm, err := cipher.NewGCM(block)
contract.AssertNoErrorf(err, "error creating AES-GCM cipher")
msg, err := aesgcm.Open(nil, nonce, ciphertext, nil)
return string(msg), err
}
// Crypter that just adds a prefix to the plaintext string when encrypting,
// and removes the prefix from the ciphertext when decrypting, for use in tests.
type prefixCrypter struct {
prefix string
}
func newPrefixCrypter(prefix string) Crypter {
return prefixCrypter{prefix: prefix}
}
func (c prefixCrypter) DecryptValue(ctx context.Context, ciphertext string) (string, error) {
return strings.TrimPrefix(ciphertext, c.prefix), nil
}
func (c prefixCrypter) EncryptValue(ctx context.Context, plaintext string) (string, error) {
return c.prefix + plaintext, nil
}
func (c prefixCrypter) BulkDecrypt(ctx context.Context, ciphertexts []string) (map[string]string, error) {
return DefaultBulkDecrypt(ctx, c, ciphertexts)
}
// DefaultBulkDecrypt decrypts a list of ciphertexts. Each ciphertext is decrypted individually. The returned
// map maps from ciphertext to plaintext. This should only be used by implementers of Decrypter to implement
// their BulkDecrypt method in cases where they can't do more efficient than just individual decryptions.
func DefaultBulkDecrypt(ctx context.Context,
decrypter Decrypter, ciphertexts []string,
) (map[string]string, error) {
if len(ciphertexts) == 0 {
return nil, nil
}
secretMap := map[string]string{}
for _, ct := range ciphertexts {
pt, err := decrypter.DecryptValue(ctx, ct)
if err != nil {
return nil, err
}
secretMap[ct] = pt
}
return secretMap, nil
}
type base64Crypter struct{}
// Base64Crypter is a Crypter that "encrypts" by encoding the string to base64.
var Base64Crypter Crypter = &base64Crypter{}
func (c *base64Crypter) EncryptValue(ctx context.Context, s string) (string, error) {
return base64.StdEncoding.EncodeToString([]byte(s)), nil
}
func (c *base64Crypter) DecryptValue(ctx context.Context, s string) (string, error) {
b, err := base64.StdEncoding.DecodeString(s)
if err != nil {
return "", err
}
return string(b), nil
}
func (c *base64Crypter) BulkDecrypt(ctx context.Context, ciphertexts []string) (map[string]string, error) {
return DefaultBulkDecrypt(ctx, c, ciphertexts)
}