pulumi/cmd/crypto_local.go
Matt Ellis 70e16a2acd Allow using the passphrase secrets manager with the pulumi service
This change allows using the passphrase secrets manager when creating
a stack managed by the Pulumi service.  `pulumi stack init`, `pulumi
new` and `pulumi up` all learned a new optional argument
`--secrets-provider` which can be set to "passphrase" to force the
passphrase based secrets provider to be used.  When unset the default
secrets provider is used based on the backend (for local stacks this
is passphrase, for remote stacks, it is the key managed by the pulumi
service).

As part of this change, we also initialize the secrets manager when a
stack is created, instead of waiting for the first time a secret
config value is stored. We do this so that if an update is run using
`pulumi.secret` before any secret configuration values are used, we
already have the correct encryption method selected for a stack.
2019-05-10 17:07:52 -07:00

118 lines
3.5 KiB
Go

// Copyright 2016-2019, 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 cmd
import (
cryptorand "crypto/rand"
"encoding/base64"
"fmt"
"os"
"github.com/pulumi/pulumi/pkg/diag"
"github.com/pulumi/pulumi/pkg/resource/config"
"github.com/pulumi/pulumi/pkg/secrets"
"github.com/pulumi/pulumi/pkg/secrets/passphrase"
"github.com/pulumi/pulumi/pkg/tokens"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
"github.com/pulumi/pulumi/pkg/util/contract"
"github.com/pulumi/pulumi/pkg/workspace"
)
func readPassphrase(prompt string) (string, error) {
if phrase := os.Getenv("PULUMI_CONFIG_PASSPHRASE"); phrase != "" {
return phrase, nil
}
return cmdutil.ReadConsoleNoEcho(prompt)
}
func newPassphraseSecretsManager(stackName tokens.QName, configFile string) (secrets.Manager, error) {
contract.Assertf(stackName != "", "stackName %s", "!= \"\"")
if configFile == "" {
f, err := workspace.DetectProjectStackPath(stackName)
if err != nil {
return nil, err
}
configFile = f
}
info, err := workspace.LoadProjectStack(configFile)
if err != nil {
return nil, err
}
// If we have a salt, we can just use it.
if info.EncryptionSalt != "" {
for {
phrase, phraseErr := readPassphrase("Enter your passphrase to unlock config/secrets\n" +
" (set PULUMI_CONFIG_PASSPHRASE to remember)")
if phraseErr != nil {
return nil, phraseErr
}
sm, smerr := passphrase.NewPassphaseSecretsManager(phrase, info.EncryptionSalt)
switch {
case smerr == passphrase.ErrIncorrectPassphrase:
cmdutil.Diag().Errorf(diag.Message("", "incorrect passphrase"))
continue
case smerr != nil:
return nil, smerr
default:
return sm, nil
}
}
}
var phrase string
// Get a the passphrase from the user, ensuring that they match.
for {
// Here, the stack does not have an EncryptionSalt, so we will get a passphrase and create one
first, err := readPassphrase("Enter your passphrase to protect config/secrets")
if err != nil {
return nil, err
}
second, err := readPassphrase("Re-enter your passphrase to confirm")
if err != nil {
return nil, err
}
if first == second {
phrase = first
break
}
// If they didn't match, print an error and try again
cmdutil.Diag().Errorf(diag.Message("", "passphrases do not match"))
}
// Produce a new salt.
salt := make([]byte, 8)
_, err = cryptorand.Read(salt)
contract.Assertf(err == nil, "could not read from system random")
// Encrypt a message and store it with the salt so we can test if the password is correct later.
crypter := config.NewSymmetricCrypterFromPassphrase(phrase, salt)
msg, err := crypter.EncryptValue("pulumi")
contract.AssertNoError(err)
// Now store the result and save it.
info.EncryptionSalt = fmt.Sprintf("v1:%s:%s", base64.StdEncoding.EncodeToString(salt), msg)
if err = info.Save(configFile); err != nil {
return nil, err
}
// Finally, build the full secrets manager from the state we just saved
return passphrase.NewPassphaseSecretsManager(phrase, info.EncryptionSalt)
}