Now, instead of having a .pulumi folder next to each project, we have a single .pulumi folder in the root of the repository. This is created by running `pulumi init`. When run in a git repository, `pulumi init` will place the .pulumi file next to the .git folder, so it can be shared across all projects in a repository. When not in a git repository, it will be created in the current working directory. We also start tracking information about the repository itself, in a new `repo.json` file stored in the root of the .pulumi folder. The information we track are "owner" and "name" which map to information we use on pulumi.com. When run in a git repository with a remote named origin pointing to a GitHub project, we compute the owner and name by deconstructing information from the remote's URL. Otherwise, we just use the current user's username and the name of the current working directory as the owner and name, respectively.
158 lines
4.3 KiB
Go
158 lines
4.3 KiB
Go
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
cryptorand "crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/pulumi/pulumi/pkg/resource/config"
|
|
"github.com/pulumi/pulumi/pkg/util/contract"
|
|
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
const aes256GCMKeyBytes = 32
|
|
|
|
// blindingDecrypter is a config.ValueDecrypter that instead of decrypting data, just returns "********", 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.
|
|
type blindingDecrypter struct{}
|
|
|
|
func (b blindingDecrypter) DecryptValue(ciphertext string) (string, error) {
|
|
return "********", nil
|
|
}
|
|
|
|
// This implements to config.EncrypterDecrypter interface but panics if any methods are called.
|
|
type panicCrypter struct{}
|
|
|
|
func (p panicCrypter) EncryptValue(plaintext string) (string, error) {
|
|
panic("attempt to encrypt value")
|
|
}
|
|
|
|
func (p panicCrypter) DecryptValue(ciphertext string) (string, error) {
|
|
panic("attempt to decrypt value")
|
|
}
|
|
|
|
// symmetricCrypter 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.
|
|
type symmetricCrypter struct {
|
|
key []byte
|
|
}
|
|
|
|
func (s symmetricCrypter) EncryptValue(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(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 "", errors.Wrap(err, "bad value")
|
|
}
|
|
|
|
enc, err := base64.StdEncoding.DecodeString(vals[2])
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "bad value")
|
|
}
|
|
|
|
return decryptAES256GCM(enc, s.key, nonce)
|
|
}
|
|
|
|
// given a passphrase and an encryption state, construct a config.ValueEncrypterDecrypter from it. Our encryption
|
|
// state value is a version tag followed by version specific state information. Presently, we only have one version
|
|
// we support (`v1`) which is AES-256-GCM using a key derived from a passphrase using 1,000,000 iterations of PDKDF2
|
|
// using SHA256.
|
|
func symmetricCrypterFromPhraseAndState(phrase string, state string) (config.ValueEncrypterDecrypter, error) {
|
|
splits := strings.SplitN(state, ":", 3)
|
|
if len(splits) != 3 {
|
|
return nil, errors.New("malformed state value")
|
|
}
|
|
|
|
if splits[0] != "v1" {
|
|
return nil, errors.New("unknown state version")
|
|
}
|
|
|
|
salt, err := base64.StdEncoding.DecodeString(splits[1])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
key := keyFromPassPhrase(phrase, salt, aes256GCMKeyBytes)
|
|
|
|
decrypter := symmetricCrypter{key: key}
|
|
|
|
decrypted, err := decrypter.DecryptValue(state[indexN(state, ":", 2)+1:])
|
|
|
|
if err != nil || decrypted != "pulumi" {
|
|
return nil, errors.New("incorrect passphrase")
|
|
}
|
|
|
|
return symmetricCrypter{key: key}, nil
|
|
}
|
|
|
|
func indexN(s string, substr string, n int) int {
|
|
contract.Require(n > 0, "n")
|
|
scratch := s
|
|
|
|
for i := n; i > 0; i-- {
|
|
idx := strings.Index(scratch, substr)
|
|
if i == -1 {
|
|
return -1
|
|
}
|
|
|
|
scratch = scratch[idx+1:]
|
|
}
|
|
|
|
return len(s) - (len(scratch) + len(substr))
|
|
}
|
|
|
|
// encryptAES256GCGM returns the ciphertext and the generated nonce
|
|
func encryptAES256GCGM(plaintext string, key []byte) ([]byte, []byte) {
|
|
contract.Requiref(len(key) == aes256GCMKeyBytes, "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.Assert(err == nil)
|
|
|
|
aesgcm, err := cipher.NewGCM(block)
|
|
contract.Assert(err == nil)
|
|
|
|
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) == aes256GCMKeyBytes, "key", "AES-256-GCM needs a 32 byte key")
|
|
|
|
block, err := aes.NewCipher(key)
|
|
contract.Assert(err == nil)
|
|
|
|
aesgcm, err := cipher.NewGCM(block)
|
|
contract.Assert(err == nil)
|
|
|
|
msg, err := aesgcm.Open(nil, nonce, ciphertext, nil)
|
|
|
|
return string(msg), err
|
|
}
|