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 }