| // Copyright 2017 The LUCI Authors. |
| // |
| // 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 main |
| |
| import ( |
| "context" |
| "io/ioutil" |
| "os" |
| "strings" |
| |
| "golang.org/x/oauth2" |
| "google.golang.org/api/option" |
| |
| "github.com/maruel/subcommands" |
| |
| cloudkms "cloud.google.com/go/kms/apiv1" |
| |
| "go.chromium.org/luci/auth" |
| "go.chromium.org/luci/auth/client/authcli" |
| "go.chromium.org/luci/common/cli" |
| "go.chromium.org/luci/common/errors" |
| "go.chromium.org/luci/common/logging" |
| ) |
| |
| type commonFlags struct { |
| subcommands.CommandRunBase |
| authFlags authcli.Flags |
| parsedAuthOpts auth.Options |
| keyPath string |
| } |
| |
| func (c *commonFlags) Init(authOpts auth.Options) { |
| c.authFlags.Register(&c.Flags, authOpts) |
| } |
| |
| func (c *commonFlags) Parse(args []string) error { |
| var err error |
| c.parsedAuthOpts, err = c.authFlags.Options() |
| if err != nil { |
| return err |
| } |
| |
| if len(args) < 1 { |
| return errors.New("positional arguments missing") |
| } |
| if len(args) > 1 { |
| return errors.New("unexpected positional arguments") |
| } |
| if err := validateCryptoKeysKMSPath(args[0]); err != nil { |
| return err |
| } |
| c.keyPath = args[0] |
| |
| return nil |
| } |
| |
| func (c *commonFlags) createAuthTokenSource(ctx context.Context) (oauth2.TokenSource, error) { |
| a := auth.NewAuthenticator(ctx, auth.SilentLogin, c.parsedAuthOpts) |
| if err := a.CheckLoginRequired(); err != nil { |
| return nil, errors.Annotate(err, "please login with `luci-auth login`").Err() |
| } |
| return a.TokenSource() |
| } |
| |
| func (c *commonFlags) commonMain(ctx context.Context) (*cloudkms.KeyManagementClient, error) { |
| // Set up service. |
| authTS, err := c.createAuthTokenSource(ctx) |
| if err != nil { |
| return nil, err |
| } |
| client, err := cloudkms.NewKeyManagementClient(ctx, option.WithTokenSource(authTS)) |
| if err != nil { |
| return nil, err |
| } |
| |
| return client, nil |
| } |
| |
| func readInput(file string) ([]byte, error) { |
| if file == "-" { |
| return ioutil.ReadAll(os.Stdin) |
| } |
| return ioutil.ReadFile(file) |
| } |
| |
| func readInputFd(file string) (*os.File, error) { |
| if file == "-" { |
| return os.Stdin, nil |
| } |
| return os.Open(file) |
| } |
| |
| func writeOutput(file string, data []byte) error { |
| if file == "-" { |
| _, err := os.Stdout.Write(data) |
| return err |
| } |
| return ioutil.WriteFile(file, data, 0664) |
| } |
| |
| // cryptoKeysPathComponents are the path components necessary for API calls related to |
| // crypto keys. |
| // |
| // This structure represents the following path format: |
| // projects/.../locations/.../keyRings/.../cryptoKeys/... |
| var cryptoKeysPathComponents = []string{ |
| "projects", |
| "locations", |
| "keyRings", |
| "cryptoKeys", |
| "cryptoKeyVersions", |
| } |
| |
| // validateCryptoKeysKMSPath validates a cloudkms path used for the API calls currently |
| // supported by this client. |
| // |
| // What this means is we only care about paths that look exactly like the ones |
| // constructed from kmsPathComponents. |
| func validateCryptoKeysKMSPath(path string) error { |
| if path[0] == '/' { |
| path = path[1:] |
| } |
| components := strings.Split(path, "/") |
| if len(components) < (len(cryptoKeysPathComponents)-1)*2 || len(components) > len(cryptoKeysPathComponents)*2 { |
| return errors.Reason("path should have the form %s", strings.Join(cryptoKeysPathComponents, "/.../")+"/...").Err() |
| } |
| for i, c := range components { |
| if i%2 == 1 { |
| continue |
| } |
| expect := cryptoKeysPathComponents[i/2] |
| if c != expect { |
| return errors.Reason("expected component %d to be %s, got %s", i+1, expect, c).Err() |
| } |
| } |
| return nil |
| } |
| |
| type verifyRun struct { |
| commonFlags |
| input string |
| inputSig string |
| doVerify func(ctx context.Context, client *cloudkms.KeyManagementClient, input *os.File, inputSig []byte, keyPath string) error |
| } |
| |
| func (v *verifyRun) Init(authOpts auth.Options) { |
| v.commonFlags.Init(authOpts) |
| v.Flags.StringVar(&v.input, "input", "", "Path to file with data to verify (use '-' for stdin).") |
| v.Flags.StringVar(&v.inputSig, "input-sig", "", "Path to read signature from (use '-' for stdin).") |
| } |
| |
| func (v *verifyRun) Parse(ctx context.Context, args []string) error { |
| if err := v.commonFlags.Parse(args); err != nil { |
| return err |
| } |
| if v.input == "" { |
| return errors.New("input file is required") |
| } |
| if v.inputSig == "" { |
| return errors.New("input signature is required") |
| } |
| return nil |
| } |
| |
| func (v *verifyRun) main(ctx context.Context) error { |
| service, err := v.commonMain(ctx) |
| if err != nil { |
| return err |
| } |
| |
| // Open input file descriptor. |
| fd, err := readInputFd(v.input) |
| if err != nil { |
| return err |
| } |
| defer fd.Close() |
| |
| // Read in signature. |
| sigBytes, err := readInput(v.inputSig) |
| if err != nil { |
| return err |
| } |
| |
| return v.doVerify(ctx, service, fd, sigBytes, v.keyPath) |
| } |
| |
| func (v *verifyRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| ctx := cli.GetContext(a, v, env) |
| if err := v.Parse(ctx, args); err != nil { |
| logging.WithError(err).Errorf(ctx, "Error while parsing arguments") |
| return 1 |
| } |
| if err := v.main(ctx); err != nil { |
| logging.WithError(err).Errorf(ctx, "Error while executing command") |
| return 1 |
| } |
| return 0 |
| } |
| |
| type signRun struct { |
| commonFlags |
| input string |
| output string |
| doSign func(ctx context.Context, client *cloudkms.KeyManagementClient, input *os.File, keyPath string) ([]byte, error) |
| } |
| |
| func (s *signRun) Init(authOpts auth.Options) { |
| s.commonFlags.Init(authOpts) |
| s.Flags.StringVar(&s.input, "input", "", "Path to file with data to sign (use '-' for stdin).") |
| s.Flags.StringVar(&s.output, "output", "", "Path to write signature to (use '-' for stdout).") |
| } |
| |
| func (s *signRun) Parse(ctx context.Context, args []string) error { |
| if err := s.commonFlags.Parse(args); err != nil { |
| return err |
| } |
| if s.input == "" { |
| return errors.New("input file is required") |
| } |
| if s.output == "" { |
| return errors.New("output location is required") |
| } |
| return nil |
| } |
| |
| func (s *signRun) main(ctx context.Context) error { |
| service, err := s.commonMain(ctx) |
| if err != nil { |
| return err |
| } |
| |
| // Read in input. |
| fd, err := readInputFd(s.input) |
| if err != nil { |
| return err |
| } |
| defer fd.Close() |
| |
| result, err := s.doSign(ctx, service, fd, s.keyPath) |
| if err != nil { |
| return err |
| } |
| |
| // Write output. |
| return writeOutput(s.output, result) |
| } |
| |
| func (s *signRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| ctx := cli.GetContext(a, s, env) |
| if err := s.Parse(ctx, args); err != nil { |
| logging.WithError(err).Errorf(ctx, "Error while parsing arguments") |
| return 1 |
| } |
| if err := s.main(ctx); err != nil { |
| logging.WithError(err).Errorf(ctx, "Error while executing command") |
| return 1 |
| } |
| return 0 |
| } |
| |
| type cryptRun struct { |
| commonFlags |
| input string |
| output string |
| doCrypt func(ctx context.Context, client *cloudkms.KeyManagementClient, input []byte, keyPath string) ([]byte, error) |
| } |
| |
| func (c *cryptRun) Init(authOpts auth.Options) { |
| c.commonFlags.Init(authOpts) |
| c.Flags.StringVar(&c.input, "input", "", "Path to file with data to operate on (use '-' for stdin). Data for encrypt and decrypt cannot be larger than 64KiB.") |
| c.Flags.StringVar(&c.output, "output", "", "Path to write operation results to (use '-' for stdout).") |
| } |
| |
| func (c *cryptRun) Parse(ctx context.Context, args []string) error { |
| if err := c.commonFlags.Parse(args); err != nil { |
| return err |
| } |
| if c.input == "" { |
| return errors.New("input file is required") |
| } |
| if c.output == "" { |
| return errors.New("output location is required") |
| } |
| return nil |
| } |
| |
| func (c *cryptRun) main(ctx context.Context) error { |
| service, err := c.commonMain(ctx) |
| if err != nil { |
| return err |
| } |
| |
| // Read in input. |
| bytes, err := readInput(c.input) |
| if err != nil { |
| return err |
| } |
| |
| result, err := c.doCrypt(ctx, service, bytes, c.keyPath) |
| if err != nil { |
| return err |
| } |
| |
| // Write output. |
| return writeOutput(c.output, result) |
| } |
| |
| func (c *cryptRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| ctx := cli.GetContext(a, c, env) |
| if err := c.Parse(ctx, args); err != nil { |
| logging.WithError(err).Errorf(ctx, "Error while parsing arguments") |
| return 1 |
| } |
| if err := c.main(ctx); err != nil { |
| logging.WithError(err).Errorf(ctx, "Error while executing command") |
| return 1 |
| } |
| return 0 |
| } |
| |
| type downloadRun struct { |
| commonFlags |
| output string |
| doDownload func(ctx context.Context, client *cloudkms.KeyManagementClient, keyPath string) ([]byte, error) |
| } |
| |
| func (d *downloadRun) Init(authOpts auth.Options) { |
| d.commonFlags.Init(authOpts) |
| d.Flags.StringVar(&d.output, "output", "", "Path to write key to (use '-' for stdout).") |
| } |
| |
| func (d *downloadRun) Parse(ctx context.Context, args []string) error { |
| if err := d.commonFlags.Parse(args); err != nil { |
| return err |
| } |
| if d.output == "" { |
| return errors.New("output location is required") |
| } |
| return nil |
| } |
| |
| func (d *downloadRun) main(ctx context.Context) error { |
| service, err := d.commonMain(ctx) |
| if err != nil { |
| return err |
| } |
| |
| result, err := d.doDownload(ctx, service, d.keyPath) |
| if err != nil { |
| return err |
| } |
| |
| // Write output. |
| return writeOutput(d.output, result) |
| } |
| |
| func (d *downloadRun) Run(a subcommands.Application, args []string, env subcommands.Env) int { |
| ctx := cli.GetContext(a, d, env) |
| if err := d.Parse(ctx, args); err != nil { |
| logging.WithError(err).Errorf(ctx, "Error while parsing arguments") |
| return 1 |
| } |
| if err := d.main(ctx); err != nil { |
| logging.WithError(err).Errorf(ctx, "Error while executing command") |
| return 1 |
| } |
| return 0 |
| } |