github.com/cdmixer/woolloomooloo@v0.1.0/pkg/cmd/pulumi/crypto_local.go (about)

     1  // Copyright 2016-2019, Pulumi Corporation.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	cryptorand "crypto/rand"
    19  	"encoding/base64"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  
    26  	"github.com/pkg/errors"
    27  	"github.com/pulumi/pulumi/pkg/v2/secrets"
    28  	"github.com/pulumi/pulumi/pkg/v2/secrets/passphrase"
    29  	"github.com/pulumi/pulumi/sdk/v2/go/common/diag"
    30  	"github.com/pulumi/pulumi/sdk/v2/go/common/resource/config"
    31  	"github.com/pulumi/pulumi/sdk/v2/go/common/tokens"
    32  	"github.com/pulumi/pulumi/sdk/v2/go/common/util/cmdutil"
    33  	"github.com/pulumi/pulumi/sdk/v2/go/common/util/contract"
    34  	"github.com/pulumi/pulumi/sdk/v2/go/common/workspace"
    35  )
    36  
    37  func readPassphrase(prompt string) (phrase string, interactive bool, err error) {
    38  	if phrase, ok := os.LookupEnv("PULUMI_CONFIG_PASSPHRASE"); ok {
    39  		return phrase, false, nil
    40  	}
    41  	if phraseFile, ok := os.LookupEnv("PULUMI_CONFIG_PASSPHRASE_FILE"); ok {
    42  		phraseFilePath, err := filepath.Abs(phraseFile)
    43  		if err != nil {
    44  			return "", false, errors.Wrap(err, "unable to construct a path the PULUMI_CONFIG_PASSPHRASE_FILE")
    45  		}
    46  		phraseDetails, err := ioutil.ReadFile(phraseFilePath)
    47  		if err != nil {
    48  			return "", false, errors.Wrap(err, "unable to read PULUMI_CONFIG_PASSPHRASE_FILE")
    49  		}
    50  		return strings.TrimSpace(string(phraseDetails)), false, nil
    51  	}
    52  	if !cmdutil.Interactive() {
    53  		return "", false, errors.New("passphrase must be set with PULUMI_CONFIG_PASSPHRASE or " +
    54  			"PULUMI_CONFIG_PASSPHRASE_FILE environment variables")
    55  	}
    56  	phrase, err = cmdutil.ReadConsoleNoEcho(prompt)
    57  	return phrase, true, err
    58  }
    59  
    60  func newPassphraseSecretsManager(stackName tokens.QName, configFile string,
    61  	rotatePassphraseSecretsProvider bool) (secrets.Manager, error) {
    62  	contract.Assertf(stackName != "", "stackName %s", "!= \"\"")
    63  
    64  	if configFile == "" {
    65  		f, err := workspace.DetectProjectStackPath(stackName)
    66  		if err != nil {
    67  			return nil, err
    68  		}
    69  		configFile = f
    70  	}
    71  
    72  	info, err := workspace.LoadProjectStack(configFile)
    73  	if err != nil {
    74  		return nil, err
    75  	}
    76  
    77  	if rotatePassphraseSecretsProvider {
    78  		info.EncryptionSalt = ""
    79  	}
    80  
    81  	// If we have a salt, we can just use it.
    82  	if info.EncryptionSalt != "" {
    83  		for {
    84  			phrase, interactive, phraseErr := readPassphrase("Enter your passphrase to unlock config/secrets\n" +
    85  				"    (set PULUMI_CONFIG_PASSPHRASE or PULUMI_CONFIG_PASSPHRASE_FILE to remember)")
    86  			if phraseErr != nil {
    87  				return nil, phraseErr
    88  			}
    89  
    90  			sm, smerr := passphrase.NewPassphaseSecretsManager(phrase, info.EncryptionSalt)
    91  			switch {
    92  			case interactive && smerr == passphrase.ErrIncorrectPassphrase:
    93  				cmdutil.Diag().Errorf(diag.Message("", "incorrect passphrase"))
    94  				continue
    95  			case smerr != nil:
    96  				return nil, smerr
    97  			default:
    98  				return sm, nil
    99  			}
   100  		}
   101  	}
   102  
   103  	var phrase string
   104  
   105  	// Get a the passphrase from the user, ensuring that they match.
   106  	for {
   107  		firstMessage := "Enter your passphrase to protect config/secrets"
   108  		if rotatePassphraseSecretsProvider {
   109  			firstMessage = "Enter your new passphrase to protect config/secrets"
   110  		}
   111  		// Here, the stack does not have an EncryptionSalt, so we will get a passphrase and create one
   112  		first, _, err := readPassphrase(firstMessage)
   113  		if err != nil {
   114  			return nil, err
   115  		}
   116  		secondMessage := "Re-enter your passphrase to confirm"
   117  		if rotatePassphraseSecretsProvider {
   118  			secondMessage = "Re-enter your new passphrase to confirm"
   119  		}
   120  		second, _, err := readPassphrase(secondMessage)
   121  		if err != nil {
   122  			return nil, err
   123  		}
   124  
   125  		if first == second {
   126  			phrase = first
   127  			break
   128  		}
   129  		// If they didn't match, print an error and try again
   130  		cmdutil.Diag().Errorf(diag.Message("", "passphrases do not match"))
   131  	}
   132  
   133  	// Produce a new salt.
   134  	salt := make([]byte, 8)
   135  	_, err = cryptorand.Read(salt)
   136  	contract.Assertf(err == nil, "could not read from system random")
   137  
   138  	// Encrypt a message and store it with the salt so we can test if the password is correct later.
   139  	crypter := config.NewSymmetricCrypterFromPassphrase(phrase, salt)
   140  	msg, err := crypter.EncryptValue("pulumi")
   141  	contract.AssertNoError(err)
   142  
   143  	// Now store the result and save it.
   144  	info.EncryptionSalt = fmt.Sprintf("v1:%s:%s", base64.StdEncoding.EncodeToString(salt), msg)
   145  	if err = info.Save(configFile); err != nil {
   146  		return nil, err
   147  	}
   148  
   149  	// Finally, build the full secrets manager from the state we just saved
   150  	return passphrase.NewPassphaseSecretsManager(phrase, info.EncryptionSalt)
   151  }