go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/cmd/secret-tool/main.go (about)

     1  // Copyright 2022 The LUCI Authors.
     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  // Executable secret-tool allows to generate and rotate secrets stored in
    16  // Google Secret Manager and consumed by go.chromium.org/luci/server/secrets
    17  // module.
    18  //
    19  // Is supports generation and manipulation of secrets that are:
    20  //   - Randomly generated byte blobs.
    21  //   - Password-like strings passed via terminal.
    22  //   - Tink key sets serialized as JSON.
    23  //
    24  // By default it doesn't access secrets once they are stored. The set of active
    25  // secrets is represented by individual GSM SecretVersion objects with aliases
    26  // "current", "previous" and "next" pointing to them. The tool knows how to move
    27  // these aliases to perform somewhat graceful rotations. When using Tink keys,
    28  // the final key set used at runtime is assembled dynamically from keys stored
    29  // in "current", "previous" and "next" SecretVersions.
    30  //
    31  // To generate a new secret, run e.g.
    32  //
    33  //	secret-tool create sm://<project>/root-secret -secret-type random-bytes-32
    34  //	secret-tool create sm://<project>/tink-aead-primary -secret-type tink-aes256-gcm
    35  //
    36  // To rotate an existing secret (regardless of its type):
    37  //
    38  //	secret-tool rotation-begin sm://<project>/<name>
    39  //	# wait several hours to make sure the new secret is cached everywhere
    40  //	# confirm by looking at /chrome/infra/secrets/gsm/version metric
    41  //	secret-tool rotation-end sm://<project>/<name>
    42  package main
    43  
    44  import (
    45  	"bytes"
    46  	"context"
    47  	"crypto/rand"
    48  	"fmt"
    49  	"os"
    50  	"sort"
    51  	"strconv"
    52  	"strings"
    53  	"syscall"
    54  
    55  	secretmanager "cloud.google.com/go/secretmanager/apiv1"
    56  	"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
    57  	"github.com/google/tink/go/aead"
    58  	"github.com/google/tink/go/insecurecleartextkeyset"
    59  	"github.com/google/tink/go/keyset"
    60  	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
    61  	"github.com/maruel/subcommands"
    62  	"golang.org/x/term"
    63  	"google.golang.org/api/iterator"
    64  	"google.golang.org/api/option"
    65  	"google.golang.org/grpc/codes"
    66  	"google.golang.org/grpc/status"
    67  	"google.golang.org/protobuf/types/known/fieldmaskpb"
    68  
    69  	"go.chromium.org/luci/auth"
    70  	"go.chromium.org/luci/auth/client/authcli"
    71  	"go.chromium.org/luci/common/cli"
    72  	"go.chromium.org/luci/common/data/stringset"
    73  	"go.chromium.org/luci/common/data/text"
    74  	"go.chromium.org/luci/common/errors"
    75  	"go.chromium.org/luci/common/flag/fixflagpos"
    76  	"go.chromium.org/luci/common/flag/flagenum"
    77  	"go.chromium.org/luci/common/flag/stringmapflag"
    78  	"go.chromium.org/luci/common/logging"
    79  	"go.chromium.org/luci/common/logging/gologger"
    80  	"go.chromium.org/luci/hardcoded/chromeinfra"
    81  )
    82  
    83  // TODO(vadimsh): Add a flag or something to instruct `rotation-begin` that it
    84  // should read Tink keys the previous `current` value and append them to the
    85  // new keyset. That way we can use old Tink keys for arbitrary long time, while
    86  // still reading only 3 secrets at runtime. In the current implementation, a
    87  // Tink key is completely forgotten after two rotations. If that's OK or not
    88  // depends on how the key is used (and thus this behavior should be controllable
    89  // by a flag or some kind of annotation).
    90  
    91  ////////////////////////////////////////////////////////////////////////////////
    92  // CLI boilerplate.
    93  
    94  var userError = errors.BoolTag{Key: errors.NewTagKey("user error")}
    95  
    96  var authOpts = chromeinfra.SetDefaultAuthOptions(auth.Options{
    97  	Scopes: []string{
    98  		"https://www.googleapis.com/auth/cloud-platform",
    99  		"https://www.googleapis.com/auth/userinfo.email",
   100  	},
   101  })
   102  
   103  type flagState string
   104  
   105  func (s flagState) shouldRegister() bool {
   106  	return s != "" && s != "disable"
   107  }
   108  
   109  var (
   110  	disableFlag  flagState = "disable"
   111  	requireFlag  flagState = "require"
   112  	optionalFlag flagState = "optional"
   113  )
   114  
   115  func main() {
   116  	os.Exit(subcommands.Run(&cli.Application{
   117  		Name:  "secret-tool",
   118  		Title: "Tool for creating and rotating secrets stored in Google Secret Manager and used by LUCI servers.",
   119  		Context: func(ctx context.Context) context.Context {
   120  			return (&gologger.LoggerConfig{
   121  				Out:    os.Stderr,
   122  				Format: `%{color}%{message}%{color:reset}`,
   123  			}).Use(ctx)
   124  		},
   125  		Commands: []*subcommands.Command{
   126  			subcommands.CmdHelp,
   127  
   128  			authcli.SubcommandLogin(authOpts, "login", false),
   129  			authcli.SubcommandLogout(authOpts, "logout", false),
   130  
   131  			{
   132  				UsageLine: "create sm://<project>/<name> -secret-type <type>",
   133  				ShortDesc: "creates a new secret",
   134  				LongDesc: text.Doc(fmt.Sprintf(`
   135  					Creates a new secret populating its value based on <type>.
   136  
   137  					Supported types:
   138  %s
   139  
   140  					All Tink keysets are stored as clear text JSONPB.
   141  				`, generatorsHelp("					  * "))),
   142  				CommandRun: func() subcommands.CommandRun {
   143  					return initCommand(&commandRun{
   144  						exec:           (*commandRun).cmdCreate,
   145  						secretTypeFlag: requireFlag,
   146  						aliasesFlag:    disableFlag,
   147  						forceFlag:      optionalFlag,
   148  					})
   149  				},
   150  			},
   151  
   152  			{
   153  				UsageLine: "inspect sm://<project>/<name>",
   154  				ShortDesc: "shows the current state of a secret",
   155  				LongDesc: text.Doc(`
   156  					Shows the current state of a secret, in particular values of aliases
   157  					denoting the current, previous and next versions of the secret.
   158  				`),
   159  				CommandRun: func() subcommands.CommandRun {
   160  					return initCommand(&commandRun{
   161  						exec:           (*commandRun).cmdInspect,
   162  						secretTypeFlag: disableFlag,
   163  						aliasesFlag:    disableFlag,
   164  						forceFlag:      disableFlag,
   165  					})
   166  				},
   167  			},
   168  
   169  			{
   170  				UsageLine: "set-aliases sm://<project>/<name>",
   171  				ShortDesc: "moves version aliases on the secret",
   172  				LongDesc: text.Doc(`
   173  					Moves the version aliases. This should rarely be used directly, only
   174  					as a way to immediately return to some previous state.
   175  
   176  					For rotations, use rotation-begin and rotation-end subcommands which
   177  					move version aliases as well.
   178  
   179  					Aliases not mentioned in the flags are left untouched. To delete an
   180  					alias, use "-alias <name>=0".
   181  				`),
   182  				CommandRun: func() subcommands.CommandRun {
   183  					return initCommand(&commandRun{
   184  						exec:           (*commandRun).cmdSetAliases,
   185  						secretTypeFlag: disableFlag,
   186  						aliasesFlag:    requireFlag,
   187  						forceFlag:      disableFlag,
   188  					})
   189  				},
   190  			},
   191  
   192  			{
   193  				UsageLine: "rotation-begin sm://<project>/<name>",
   194  				ShortDesc: "generates a new version of the secret and designates it as next",
   195  				LongDesc: text.Doc(`
   196  					This starts the secret rotation process by generating a new version
   197  					of the secret and moving "next" alias to point to it (keeping all
   198  					other aliases intact). This allows the processes that use the secret
   199  					to precache the new version, before it is actually used.
   200  
   201  					To finish the rotation, call rotation-end at some later time when
   202  					all processes picked up the new secret. How long it takes depends
   203  					on the service configuration and can measure in hours.
   204  				`),
   205  				CommandRun: func() subcommands.CommandRun {
   206  					return initCommand(&commandRun{
   207  						exec:           (*commandRun).cmdRotationBegin,
   208  						secretTypeFlag: optionalFlag,
   209  						aliasesFlag:    disableFlag,
   210  						forceFlag:      optionalFlag,
   211  					})
   212  				},
   213  			},
   214  
   215  			{
   216  				UsageLine: "rotation-end sm://<project>/<name>",
   217  				ShortDesc: "finishes the rotation started with rotation-begin",
   218  				LongDesc: text.Doc(`
   219  					This finishes the rotation started with rotation-begin by
   220  					updating aliases as:
   221  						previous := current
   222  						current := next
   223  
   224  					This should be done once all processes cached the "next" version
   225  					of the secret. After this command finishes, this version will be used
   226  					as "current" (i.e. used for encryption, signing, etc).
   227  
   228  					Note that this completely evicts the old "previous" value (which is
   229  					a leftover from the previous rotation). Be careful when doing
   230  					rotations back to back.
   231  				`),
   232  				CommandRun: func() subcommands.CommandRun {
   233  					return initCommand(&commandRun{
   234  						exec:           (*commandRun).cmdRotationEnd,
   235  						secretTypeFlag: disableFlag,
   236  						aliasesFlag:    disableFlag,
   237  						forceFlag:      disableFlag,
   238  					})
   239  				},
   240  			},
   241  		},
   242  	}, fixflagpos.FixSubcommands(os.Args[1:])))
   243  }
   244  
   245  type commandRun struct {
   246  	subcommands.CommandRunBase
   247  	authFlags authcli.Flags
   248  
   249  	secretTypeFlag flagState // controls presence of -secret-type flag
   250  	aliasesFlag    flagState // controls presence of -alias flag
   251  	forceFlag      flagState // controls presence of -force flag
   252  
   253  	gsm        *secretmanager.Client // GSM client
   254  	project    string                // GCP project with the secret
   255  	secret     string                // name of the secret
   256  	secretRef  string                // full name of the secret for GSM
   257  	secretGen  secretGenerator       // parsed -secret-type or nil if wasn't set
   258  	aliasesRaw stringmapflag.Value   // raw collected -alias flag values
   259  	aliases    map[string]int64      // parsed -alias flags
   260  	force      bool                  // parsed -force flag
   261  
   262  	exec func(*commandRun, context.Context) error // method to call to execute the command
   263  }
   264  
   265  func initCommand(c *commandRun) *commandRun {
   266  	c.authFlags.Register(&c.Flags, authOpts)
   267  	if c.secretTypeFlag.shouldRegister() {
   268  		c.Flags.Var(&c.secretGen, "secret-type", "What kind of secret value to generate.")
   269  	}
   270  	if c.aliasesFlag.shouldRegister() {
   271  		c.Flags.Var(&c.aliasesRaw, "alias", "A name=version pair indicating an alias.")
   272  	}
   273  	if c.forceFlag.shouldRegister() {
   274  		c.Flags.BoolVar(&c.force, "force", false, "Ignore safeguards and apply the change.")
   275  	}
   276  	return c
   277  }
   278  
   279  func (c *commandRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
   280  	ctx := cli.GetContext(a, c, env)
   281  
   282  	if len(args) != 1 {
   283  		logging.Errorf(ctx, "Expecting exactly one positional argument: the secret path as sm://<project>/<name>.")
   284  		return 1
   285  	}
   286  	secretPath := args[0]
   287  
   288  	// Parse the secret reference into its components.
   289  	if !strings.HasPrefix(secretPath, "sm://") {
   290  		logging.Errorf(ctx, "Only sm:// secrets are supported.")
   291  		return 1
   292  	}
   293  	parts := strings.Split(strings.TrimPrefix(secretPath, "sm://"), "/")
   294  	if len(parts) != 2 {
   295  		logging.Errorf(ctx, "Expecting full secret reference as sm://<project>/<name>.")
   296  		return 1
   297  	}
   298  	c.project, c.secret = parts[0], parts[1]
   299  	c.secretRef = fmt.Sprintf("projects/%s/secrets/%s", c.project, c.secret)
   300  
   301  	// Check flags.
   302  	if c.secretTypeFlag == requireFlag && c.secretGen.name == "" {
   303  		logging.Errorf(ctx, "Missing required flag -secret-type.")
   304  		return 1
   305  	}
   306  	if c.aliasesFlag.shouldRegister() {
   307  		c.aliases = make(map[string]int64, len(c.aliasesRaw))
   308  		for alias, ver := range c.aliasesRaw {
   309  			verInt, err := strconv.ParseInt(ver, 10, 64)
   310  			if err != nil {
   311  				logging.Errorf(ctx, "Bad -alias flag %s=%s: the version is not an integer.", alias, ver)
   312  				return 1
   313  			}
   314  			c.aliases[alias] = verInt
   315  		}
   316  		if len(c.aliases) == 0 && c.aliasesFlag == requireFlag {
   317  			logging.Errorf(ctx, "At least one -alias <name>=<version> flag is required.")
   318  			return 1
   319  		}
   320  	}
   321  
   322  	// Setup the GSM client.
   323  	authOpts, err := c.authFlags.Options()
   324  	if err != nil {
   325  		logging.Errorf(ctx, "Bad auth options: %s.", err)
   326  		return 1
   327  	}
   328  	switch ts, err := auth.NewAuthenticator(ctx, auth.SilentLogin, authOpts).TokenSource(); {
   329  	case err == auth.ErrLoginRequired:
   330  		logging.Errorf(ctx, "Need to login first. Run `auth-login` subcommand.")
   331  		return 1
   332  	case err != nil:
   333  		errors.Log(ctx, err)
   334  		return 1
   335  	default:
   336  		c.gsm, err = secretmanager.NewClient(
   337  			ctx,
   338  			option.WithTokenSource(ts),
   339  		)
   340  		if err != nil {
   341  			errors.Log(ctx, err)
   342  			return 1
   343  		}
   344  	}
   345  
   346  	if err = c.exec(c, ctx); err != nil {
   347  		if userError.In(err) {
   348  			logging.Errorf(ctx, "%s", err)
   349  		} else {
   350  			errors.Log(ctx, err)
   351  		}
   352  		return 1
   353  	}
   354  	return 0
   355  }
   356  
   357  ////////////////////////////////////////////////////////////////////////////////
   358  // Helpers that work with the secret selected in `c.secretRef`.
   359  
   360  const secretTypeLabel = "luci-secret"
   361  
   362  // parseVersion extracts int64 version from SecretVersion name.
   363  func parseVersion(versionName string) (int64, error) {
   364  	idx := strings.LastIndex(versionName, "/")
   365  	if idx == -1 {
   366  		return 0, errors.Reason("unexpected version name format %q", versionName).Err()
   367  	}
   368  	ver, err := strconv.ParseInt(versionName[idx+1:], 10, 64)
   369  	if err != nil {
   370  		return 0, errors.Reason("unexpected version name format %q", versionName).Err()
   371  	}
   372  	if ver == 0 {
   373  		return 0, errors.Reason("the version is unexpectedly 0").Err()
   374  	}
   375  	return ver, nil
   376  }
   377  
   378  // secretMetadata fetches metadata about the secret.
   379  func (c *commandRun) secretMetadata(ctx context.Context) (*secretmanagerpb.Secret, error) {
   380  	secret, err := c.gsm.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
   381  		Name: c.secretRef,
   382  	})
   383  	if err != nil {
   384  		return nil, errors.Annotate(err, "failed to fetch the secret metadata").Err()
   385  	}
   386  	return secret, nil
   387  }
   388  
   389  // generateNewVersion generates new secret blob, adds it as SecretVersion, and
   390  // returns its version number.
   391  func (c *commandRun) generateNewVersion(ctx context.Context) (int64, error) {
   392  	logging.Infof(ctx, "Creating and storing the secret of type %q...", c.secretGen.name)
   393  	secretBlob, err := c.secretGen.gen(ctx)
   394  	if err != nil {
   395  		return 0, errors.Annotate(err, "failed to generate the secret of type %q", c.secretGen.name).Err()
   396  	}
   397  	version, err := c.gsm.AddSecretVersion(ctx, &secretmanagerpb.AddSecretVersionRequest{
   398  		Parent: c.secretRef,
   399  		Payload: &secretmanagerpb.SecretPayload{
   400  			Data: secretBlob,
   401  		},
   402  	})
   403  	if err != nil {
   404  		return 0, errors.Annotate(err, "failed to add the new secret version").Err()
   405  	}
   406  	ver, err := parseVersion(version.Name)
   407  	if err != nil {
   408  		return 0, err
   409  	}
   410  	logging.Infof(ctx, "Created the new secret version %d.", ver)
   411  	return ver, nil
   412  }
   413  
   414  // latestVersion resolves "latest" into an int64 version.
   415  func (c *commandRun) latestVersion(ctx context.Context) (int64, error) {
   416  	version, err := c.gsm.GetSecretVersion(ctx, &secretmanagerpb.GetSecretVersionRequest{
   417  		Name: fmt.Sprintf("%s/versions/latest", c.secretRef),
   418  	})
   419  	if err != nil {
   420  		return 0, errors.Annotate(err, "failed to resolve the latest version").Err()
   421  	}
   422  	return parseVersion(version.Name)
   423  }
   424  
   425  // overrideAliases overrides *all* aliases.
   426  func (c *commandRun) overrideAliases(ctx context.Context, etag string, aliases map[string]int64) (*secretmanagerpb.Secret, error) {
   427  	logging.Infof(ctx, "Updating aliases...")
   428  	secret, err := c.gsm.UpdateSecret(ctx, &secretmanagerpb.UpdateSecretRequest{
   429  		Secret: &secretmanagerpb.Secret{
   430  			Name:           c.secretRef,
   431  			Etag:           etag,
   432  			VersionAliases: aliases,
   433  		},
   434  		UpdateMask: &fieldmaskpb.FieldMask{
   435  			Paths: []string{"version_aliases"},
   436  		},
   437  	})
   438  	if err != nil {
   439  		return nil, errors.Annotate(err, "failed to set version aliases to %v", aliases).Err()
   440  	}
   441  	return secret, nil
   442  }
   443  
   444  // overrideLabels overrides *all* labels.
   445  func (c *commandRun) overrideLabels(ctx context.Context, etag string, labels map[string]string) (*secretmanagerpb.Secret, error) {
   446  	logging.Infof(ctx, "Updating labels...")
   447  	secret, err := c.gsm.UpdateSecret(ctx, &secretmanagerpb.UpdateSecretRequest{
   448  		Secret: &secretmanagerpb.Secret{
   449  			Name:   c.secretRef,
   450  			Etag:   etag,
   451  			Labels: labels,
   452  		},
   453  		UpdateMask: &fieldmaskpb.FieldMask{
   454  			Paths: []string{"labels"},
   455  		},
   456  	})
   457  	if err != nil {
   458  		return nil, errors.Annotate(err, "failed to set labels to %v", labels).Err()
   459  	}
   460  	return secret, nil
   461  }
   462  
   463  // printAliasMap prints information about current aliases.
   464  func (c *commandRun) printAliasMap(ctx context.Context, title string, secret *secretmanagerpb.Secret, showSwitchCmd bool) {
   465  	aliases := []string{"current", "previous", "next"}
   466  
   467  	logging.Infof(ctx, "%s:", title)
   468  	for _, alias := range aliases {
   469  		if ver := secret.VersionAliases[alias]; ver != 0 {
   470  			logging.Infof(ctx, "  %s = %d", alias, secret.VersionAliases[alias])
   471  		}
   472  	}
   473  	if len(secret.VersionAliases) == 0 {
   474  		logging.Infof(ctx, "  <no aliases>")
   475  	}
   476  
   477  	// Generate a command to help to hop into this state.
   478  	if showSwitchCmd {
   479  		switchCmd := []string{
   480  			"secret-tool",
   481  			"set-aliases",
   482  			fmt.Sprintf("sm://%s/%s", c.project, c.secret),
   483  		}
   484  		for _, alias := range aliases {
   485  			switchCmd = append(switchCmd, "-alias", fmt.Sprintf("%s=%d", alias, secret.VersionAliases[alias]))
   486  		}
   487  		logging.Infof(ctx, "Command to immediately switch to this state if necessary:")
   488  		logging.Infof(ctx, "  $ %s", strings.Join(switchCmd, " "))
   489  	}
   490  }
   491  
   492  // printSecretMetadata prints some information about the secret to stdout.
   493  func (c *commandRun) printSecretMetadata(ctx context.Context, secret *secretmanagerpb.Secret) {
   494  	secretType := "<unknown>"
   495  	if typ := secret.Labels[secretTypeLabel]; typ != "" {
   496  		secretType = typ
   497  	}
   498  	logging.Infof(ctx, "Secret type: %s", secretType)
   499  	c.printAliasMap(ctx, "Aliases", secret, true)
   500  }
   501  
   502  ////////////////////////////////////////////////////////////////////////////////
   503  // "create" implementation.
   504  
   505  func (c *commandRun) cmdCreate(ctx context.Context) error {
   506  	logging.Infof(ctx, "Creating the secret...")
   507  	secret, err := c.gsm.CreateSecret(ctx, &secretmanagerpb.CreateSecretRequest{
   508  		Parent:   fmt.Sprintf("projects/%s", c.project),
   509  		SecretId: c.secret,
   510  		Secret: &secretmanagerpb.Secret{
   511  			Replication: &secretmanagerpb.Replication{
   512  				Replication: &secretmanagerpb.Replication_Automatic_{},
   513  			},
   514  			Labels: map[string]string{
   515  				secretTypeLabel: c.secretGen.name,
   516  			},
   517  		},
   518  	})
   519  
   520  	if status.Code(err) == codes.AlreadyExists {
   521  		// Check if it is just an empty container that doesn't have any versions.
   522  		// This is an allowed use case. The container may be create by Terraform.
   523  		iter := c.gsm.ListSecretVersions(ctx, &secretmanagerpb.ListSecretVersionsRequest{
   524  			Parent:   c.secretRef,
   525  			PageSize: 1,
   526  		})
   527  		switch _, err := iter.Next(); {
   528  		case err == iterator.Done:
   529  			logging.Infof(ctx, "The secret already exists and has no versions, proceeding...")
   530  		case err == nil:
   531  			return errors.New(
   532  				"This secret already exists and has versions. "+
   533  					"If you want to rotate it use rotation-begin and rotation-end subcommands.", userError)
   534  		default:
   535  			return errors.Annotate(err, "failed to check if the secret has any versions").Err()
   536  		}
   537  		// Verify the type label is set, update if not.
   538  		secret, err = c.secretMetadata(ctx)
   539  		if err != nil {
   540  			return err
   541  		}
   542  		existingType := secret.Labels[secretTypeLabel]
   543  		if existingType != c.secretGen.name {
   544  			if existingType != "" && !c.force {
   545  				return errors.Reason(
   546  					"The secret already exists and its type is set to %q (not %q, as requested). "+
   547  						"Pass -force to override the type.", existingType, c.secretGen.name).Tag(userError).Err()
   548  			}
   549  			if existingType != "" {
   550  				logging.Warningf(ctx, "Overriding the secret type %q => %q.", existingType, c.secretGen.name)
   551  			}
   552  			if secret.Labels == nil {
   553  				secret.Labels = map[string]string{}
   554  			}
   555  			secret.Labels[secretTypeLabel] = c.secretGen.name
   556  			secret, err = c.overrideLabels(ctx, secret.Etag, secret.Labels)
   557  			if err != nil {
   558  				return err
   559  			}
   560  		}
   561  	} else if err != nil {
   562  		return errors.Annotate(err, "failed to create the secret").Err()
   563  	}
   564  
   565  	added, err := c.generateNewVersion(ctx)
   566  	if err != nil {
   567  		return err
   568  	}
   569  
   570  	secret, err = c.overrideAliases(ctx, secret.Etag, map[string]int64{
   571  		"current":  added,
   572  		"previous": added,
   573  		"next":     added,
   574  	})
   575  	if err != nil {
   576  		return err
   577  	}
   578  	c.printSecretMetadata(ctx, secret)
   579  	return nil
   580  }
   581  
   582  ////////////////////////////////////////////////////////////////////////////////
   583  // "inspect" implementation.
   584  
   585  func (c *commandRun) cmdInspect(ctx context.Context) error {
   586  	secret, err := c.secretMetadata(ctx)
   587  	if err != nil {
   588  		return err
   589  	}
   590  	c.printSecretMetadata(ctx, secret)
   591  	return nil
   592  }
   593  
   594  ////////////////////////////////////////////////////////////////////////////////
   595  // "set-aliases" implementation.
   596  
   597  func (c *commandRun) cmdSetAliases(ctx context.Context) error {
   598  	secret, err := c.secretMetadata(ctx)
   599  	if err != nil {
   600  		return err
   601  	}
   602  
   603  	c.printAliasMap(ctx, "Current aliases", secret, true)
   604  
   605  	for k, v := range c.aliases {
   606  		if v == 0 {
   607  			delete(secret.VersionAliases, k)
   608  		} else {
   609  			if secret.VersionAliases == nil {
   610  				secret.VersionAliases = make(map[string]int64, 1)
   611  			}
   612  			secret.VersionAliases[k] = v
   613  		}
   614  	}
   615  
   616  	secret, err = c.overrideAliases(ctx, secret.Etag, secret.VersionAliases)
   617  	if err != nil {
   618  		return err
   619  	}
   620  
   621  	c.printAliasMap(ctx, "Updated aliases", secret, false)
   622  	return nil
   623  }
   624  
   625  ////////////////////////////////////////////////////////////////////////////////
   626  // "rotation-begin" implementation.
   627  
   628  func (c *commandRun) cmdRotationBegin(ctx context.Context) error {
   629  	secret, err := c.secretMetadata(ctx)
   630  	if err != nil {
   631  		return err
   632  	}
   633  
   634  	// Figure out how to generate the new secret, populate c.secretGen.
   635  	existingType := secret.Labels[secretTypeLabel]
   636  	if existingType == "" {
   637  		if c.secretGen.name == "" {
   638  			return errors.New("This secret is not annotated with a type, pass -secret-type explicitly.", userError)
   639  		}
   640  		existingType = c.secretGen.name
   641  	}
   642  	if c.secretGen.name == "" {
   643  		typ, ok := secretTypes[existingType]
   644  		if !ok {
   645  			return errors.Reason(
   646  				"The secret is annotated with unrecognized type %q. "+
   647  					"You may need to pass -secret-type and -force flags to override, but be careful.",
   648  				existingType).Tag(userError).Err()
   649  		}
   650  		c.secretGen = typ.(secretGenerator)
   651  	}
   652  	if c.secretGen.name != existingType {
   653  		if !c.force && !c.secretGen.compatible.Has(existingType) {
   654  			return errors.Reason(
   655  				"Can't change the secret type from %q to %q. Types are incompatible. "+
   656  					"If you really need this change, pass -force flag. This is dangerous.",
   657  				existingType, c.secretGen.name,
   658  			).Tag(userError).Err()
   659  		}
   660  		logging.Warningf(ctx, "Overriding the secret type %q => %q.", existingType, c.secretGen.name)
   661  		labels := secret.Labels
   662  		if labels == nil {
   663  			labels = map[string]string{}
   664  		}
   665  		labels[secretTypeLabel] = c.secretGen.name
   666  		if secret, err = c.overrideLabels(ctx, secret.Etag, labels); err != nil {
   667  			return err
   668  		}
   669  	}
   670  
   671  	// Legacy secrets use "latest" as "current", and it is a magical alias that
   672  	// needs to be resolved via an RPC.
   673  	current := secret.VersionAliases["current"]
   674  	if current == 0 {
   675  		current, err = c.latestVersion(ctx)
   676  		if err != nil {
   677  			return err
   678  		}
   679  		logging.Infof(ctx, "This looks like a legacy secret without \"current\" alias.")
   680  		logging.Infof(ctx, "The current value of \"latest\" (%d) will be set as \"current\".", current)
   681  	}
   682  
   683  	// Abort if already rotating.
   684  	if next := secret.VersionAliases["next"]; next != 0 && next != current {
   685  		c.printAliasMap(ctx, "Current aliases", secret, false)
   686  		return errors.New("Looks like a rotation is already in progress.", userError)
   687  	}
   688  
   689  	c.printAliasMap(ctx, "Aliases prior to the starting rotation", secret, true)
   690  
   691  	// Create the new version.
   692  	next, err := c.generateNewVersion(ctx)
   693  	if err != nil {
   694  		return err
   695  	}
   696  
   697  	// Update the alias map. Don't touch existing aliases, including "previous".
   698  	if secret.VersionAliases == nil {
   699  		secret.VersionAliases = make(map[string]int64, 2)
   700  	}
   701  	secret.VersionAliases["current"] = current
   702  	secret.VersionAliases["next"] = next
   703  	secret, err = c.overrideAliases(ctx, secret.Etag, secret.VersionAliases)
   704  	if err != nil {
   705  		return err
   706  	}
   707  
   708  	c.printAliasMap(ctx, "Aliases now", secret, false)
   709  	return nil
   710  }
   711  
   712  ////////////////////////////////////////////////////////////////////////////////
   713  // "rotation-end" implementation.
   714  
   715  func (c *commandRun) cmdRotationEnd(ctx context.Context) error {
   716  	secret, err := c.secretMetadata(ctx)
   717  	if err != nil {
   718  		return err
   719  	}
   720  
   721  	c.printAliasMap(ctx, "Current aliases", secret, true)
   722  
   723  	current := secret.VersionAliases["current"]
   724  	next := secret.VersionAliases["next"]
   725  	if current == 0 || next == 0 || current == next {
   726  		return errors.New("There's no rotation in progress.", userError)
   727  	}
   728  
   729  	if secret.VersionAliases == nil {
   730  		secret.VersionAliases = make(map[string]int64, 2)
   731  	}
   732  	secret.VersionAliases["previous"] = current
   733  	secret.VersionAliases["current"] = next
   734  
   735  	secret, err = c.overrideAliases(ctx, secret.Etag, secret.VersionAliases)
   736  	if err != nil {
   737  		return err
   738  	}
   739  
   740  	c.printAliasMap(ctx, "Updated aliases", secret, false)
   741  	return nil
   742  }
   743  
   744  ////////////////////////////////////////////////////////////////////////////////
   745  // Secret generators registry.
   746  
   747  type secretGenerator struct {
   748  	name       string
   749  	help       string
   750  	compatible stringset.Set // types that can upgraded from
   751  	gen        func(context.Context) ([]byte, error)
   752  }
   753  
   754  var secretTypes = flagenum.Enum{
   755  	// populated in init()
   756  }
   757  
   758  func (gen *secretGenerator) Set(v string) error {
   759  	return secretTypes.FlagSet(gen, v)
   760  }
   761  
   762  func (gen *secretGenerator) String() string {
   763  	return gen.name
   764  }
   765  
   766  func registerGenerator(name, help string, compatible []string, gen func(context.Context) ([]byte, error)) {
   767  	compatibleTypes := stringset.NewFromSlice(compatible...)
   768  	compatibleTypes.Add(name)
   769  	secretTypes[name] = secretGenerator{
   770  		name:       name,
   771  		help:       help,
   772  		compatible: compatibleTypes,
   773  		gen:        gen,
   774  	}
   775  }
   776  
   777  func generatorsHelp(padding string) string {
   778  	lines := make([]string, 0, len(secretTypes))
   779  	for _, gen := range secretTypes {
   780  		gen := gen.(secretGenerator)
   781  		lines = append(lines, fmt.Sprintf("%s%s: %s", padding, gen.name, gen.help))
   782  	}
   783  	sort.Strings(lines)
   784  	return strings.Join(lines, "\n")
   785  }
   786  
   787  func generateTinkKey(template *tinkpb.KeyTemplate) ([]byte, error) {
   788  	kh, err := keyset.NewHandle(template)
   789  	if err != nil {
   790  		return nil, err
   791  	}
   792  	buf := &bytes.Buffer{}
   793  	if err = insecurecleartextkeyset.Write(kh, keyset.NewJSONWriter(buf)); err != nil {
   794  		return nil, err
   795  	}
   796  	return buf.Bytes(), nil
   797  }
   798  
   799  ////////////////////////////////////////////////////////////////////////////////
   800  // Supported secret generators.
   801  
   802  func init() {
   803  	registerGenerator(
   804  		"random-bytes-32",
   805  		"a random 32 byte blob",
   806  		nil,
   807  		func(context.Context) ([]byte, error) {
   808  			blob := make([]byte, 32)
   809  			_, err := rand.Read(blob)
   810  			return blob, err
   811  		},
   812  	)
   813  
   814  	registerGenerator(
   815  		"password",
   816  		"read a secret from the terminal as a password",
   817  		nil,
   818  		func(ctx context.Context) ([]byte, error) {
   819  			fmt.Printf("Type the secret value and hit Enter: ")
   820  			return term.ReadPassword(int(syscall.Stdin))
   821  		},
   822  	)
   823  
   824  	registerGenerator(
   825  		"tink-aes256-gcm",
   826  		"a generated Tink keyset with AES256 GCM key used for AEAD",
   827  		nil,
   828  		func(ctx context.Context) ([]byte, error) {
   829  			return generateTinkKey(aead.AES256GCMKeyTemplate())
   830  		},
   831  	)
   832  }