diff --git a/README.md b/README.md index 1fcc333da9..8f90c318b9 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,26 @@ $ cat ~/.docker/config.json | jq '.auths["registry.ci.openshift.org"]' "auth": "token" } ``` +## Secure Credentials Storage +The `OCM_KEYRING` environment variable provides the ability to store the ROSA +configuration containing your authentication tokens in your OS keyring. This is provided +as an alternative to storing the configuration in plain-text on your system. +`OCM_KEYRING` will override all other token or configuration related flags. + +`OCM_KEYRING` supports the following keyrings: + +* [Windows Credential Manager](https://support.microsoft.com/en-us/windows/accessing-credential-manager-1b5c916a-6a16-889f-8581-fc16e8165ac0) - `wincred` +* [macOS Keychain](https://support.apple.com/en-us/guide/keychain-access/welcome/mac) - `keychain` +* Secret Service ([Gnome Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), [KWallet](https://apps.kde.org/kwalletmanager5/), etc.) - `secret-service` +* [Pass](https://www.passwordstore.org/) - `pass` + +To ensure `OCM_KEYRING` is provided to all `rosa` commands, it is recommended to set it in your `~/.bashrc` file or equivalent. + +| | wincred | keychain | secret-service | pass | +| ------------- | ------------- | ------------- | ------------- | ------------- | +| Windows | :heavy_check_mark: | :x: | :x: | :x: | +| macOS | :x: | :heavy_check_mark: | :x: | :heavy_check_mark: | +| Linux | :x: | :x: | :heavy_check_mark: | :heavy_check_mark: | ## Have you got feedback? We want to hear it. [Open an issue](https://github.com/openshift/rosa/issues/new) against the repo and someone from the team will be in touch. diff --git a/cmd/config/cmd.go b/cmd/config/cmd.go index ebda81f76a..8a4577bb25 100644 --- a/cmd/config/cmd.go +++ b/cmd/config/cmd.go @@ -26,6 +26,7 @@ import ( "github.com/openshift/rosa/cmd/config/get" "github.com/openshift/rosa/cmd/config/set" "github.com/openshift/rosa/pkg/config" + "github.com/openshift/rosa/pkg/properties" ) func configVarDocs() string { @@ -57,7 +58,14 @@ The following variables are supported: Note that "rosa config get access_token" gives whatever the file contains - may be missing or expired; you probably want "rosa token" command instead which will obtain a fresh token if needed. -`, loc, configVarDocs()) + +If '%s' is set, the configuration file is ignored and the keyring is used instead. The +following backends are supported for the keyring: + +- macOS: keychain, pass +- Linux: secret-service, pass +- Windows: wincred +`, loc, configVarDocs(), properties.KeyringEnvKey) } func NewConfigCommand() *cobra.Command { diff --git a/cmd/config/get/get.go b/cmd/config/get/get.go index 61cd80a7df..fc659947d6 100644 --- a/cmd/config/get/get.go +++ b/cmd/config/get/get.go @@ -21,6 +21,7 @@ import ( "io" "os" + "github.com/openshift-online/ocm-sdk-go/authentication/securestore" "github.com/spf13/cobra" "github.com/openshift/rosa/pkg/config" @@ -54,20 +55,26 @@ func run(_ *cobra.Command, argv []string) { } func PrintConfig(arg string) error { - // Load the configuration file: - cfg, err := config.Load() - if err != nil { - return fmt.Errorf("Failed to load config file: %v", err) + // The following variables are not stored in the configuration + // and can skip loading configuration: + skipConfigLoadMap := map[string]bool{ + "keyrings": true, } - // If the configuration file doesn't exist yet assume that all the configuration settings - // are empty: - if cfg == nil { - loc, err := config.Location() + cfg := &config.Config{} + var err error + if !skipConfigLoadMap[arg] { + // Load the configuration: + cfg, err = config.Load() if err != nil { - return fmt.Errorf("Failed to find config file location: %v", err) + return fmt.Errorf("can't load config: %v", err) + } + // If the configuration doesn't exist yet assume that all the configuration settings + // are empty: + if cfg == nil { + fmt.Fprintf(Writer, "\n") + return nil } - return fmt.Errorf("Config file '%s' does not exist", loc) } // Print the value of the requested configuration setting: @@ -90,8 +97,24 @@ func PrintConfig(arg string) error { fmt.Fprintf(Writer, "%s\n", cfg.URL) case "fedramp": fmt.Fprintf(Writer, "%v\n", cfg.FedRAMP) + case "keyrings": + keyrings, err := getKeyrings() + if err != nil { + return err + } + for _, keyring := range keyrings { + fmt.Fprintf(Writer, "%s\n", keyring) + } default: return fmt.Errorf("'%s' is not a supported setting", arg) } return nil } + +func getKeyrings() ([]string, error) { + backends := securestore.AvailableBackends() + if len(backends) == 0 { + return backends, fmt.Errorf("error: no keyrings available") + } + return backends, nil +} diff --git a/cmd/login/cmd.go b/cmd/login/cmd.go index b8b7ce2a9d..5c695c3801 100644 --- a/cmd/login/cmd.go +++ b/cmd/login/cmd.go @@ -27,6 +27,7 @@ import ( "github.com/golang-jwt/jwt/v4" sdk "github.com/openshift-online/ocm-sdk-go" "github.com/openshift-online/ocm-sdk-go/authentication" + "github.com/openshift-online/ocm-sdk-go/authentication/securestore" "github.com/spf13/cobra" "github.com/openshift/rosa/cmd/logout" @@ -36,6 +37,7 @@ import ( "github.com/openshift/rosa/pkg/interactive" "github.com/openshift/rosa/pkg/ocm" "github.com/openshift/rosa/pkg/output" + "github.com/openshift/rosa/pkg/properties" rprtr "github.com/openshift/rosa/pkg/reporter" "github.com/openshift/rosa/pkg/rosa" ) @@ -62,14 +64,15 @@ var args struct { var Cmd = &cobra.Command{ Use: "login", Short: "Log in to your Red Hat account", - Long: fmt.Sprintf("Log in to your Red Hat account, saving the credentials to the configuration file.\n"+ + Long: fmt.Sprintf("Log in to your Red Hat account, saving the credentials to the configuration file or OS Keyring.\n"+ "The supported mechanism is by using a token, which can be obtained at: %s\n\n"+ "The application looks for the token in the following order, stopping when it finds it:\n"+ - "\t1. Command-line flags\n"+ - "\t2. Environment variable (ROSA_TOKEN)\n"+ - "\t3. Environment variable (OCM_TOKEN)\n"+ - "\t4. Configuration file\n"+ - "\t5. Command-line prompt\n", uiTokenPage), + fmt.Sprintf("\t1. OS Keyring via Environment variable (%s)\n", properties.KeyringEnvKey)+ + "\t2. Command-line flags\n"+ + "\t3. Environment variable (ROSA_TOKEN)\n"+ + "\t4. Environment variable (OCM_TOKEN)\n"+ + "\t5. Configuration file\n"+ + "\t6. Command-line prompt\n", uiTokenPage), Example: fmt.Sprintf(` # Login to the OpenShift API with an existing token generated from %s rosa login --token=$OFFLINE_ACCESS_TOKEN`, uiTokenPage), Run: run, @@ -136,7 +139,7 @@ func init() { "use-auth-code", false, "Login using OAuth Authorization Code. This should be used for most cases where a "+ - "browser is available.", + "browser is available. See --use-device-code for remote hosts and containers.", ) flags.BoolVar( &args.useDeviceCode, @@ -144,15 +147,24 @@ func init() { false, "Login using OAuth Device Code. "+ "This should only be used for remote hosts and containers where browsers are "+ - "not available. Use auth code for all other scenarios.", + "not available. See --use-auth-code for all other scenarios.", ) arguments.AddRegionFlag(flags) fedramp.AddFlag(flags) } func run(cmd *cobra.Command, argv []string) { - ctx := cmd.Context() r := rosa.NewRuntime() + defer r.Cleanup() + err := runWithRuntime(r, cmd, argv) + if err != nil { + r.Reporter.Errorf(err.Error()) + os.Exit(1) + } +} + +func runWithRuntime(r *rosa.Runtime, cmd *cobra.Command, argv []string) error { + ctx := cmd.Context() var spin *spinner.Spinner if r.Reporter.IsTerminal() && !output.HasFlag() { spin = spinner.New(spinner.CharSets[9], 100*time.Millisecond) @@ -161,8 +173,15 @@ func run(cmd *cobra.Command, argv []string) { // Check mandatory options: env := args.env if env == "" { - r.Reporter.Errorf("Option '--env' is mandatory") - os.Exit(1) + return fmt.Errorf("Option '--env' is mandatory") + } + + // Fail fast if config is keyring managed and invalid + if keyring, ok := config.IsKeyringManaged(); ok { + err := securestore.ValidateBackend(keyring) + if err != nil { + return fmt.Errorf("Error validating keyring: %v", err) + } } if args.useAuthCode { @@ -178,8 +197,7 @@ func run(cmd *cobra.Command, argv []string) { } token, err := authentication.InitiateAuthCode(oauthClientId) if err != nil { - r.Reporter.Errorf("An error occurred while retrieving the token: %v", err) - os.Exit(1) + return fmt.Errorf("An error occurred while retrieving the token: %v", err) } args.token = token args.clientID = oauthClientId @@ -190,8 +208,7 @@ func run(cmd *cobra.Command, argv []string) { } _, err := deviceAuthConfig.InitiateDeviceAuth(ctx) if err != nil { - r.Reporter.Errorf("An error occurred while initiating device auth: %v", err) - os.Exit(1) + return fmt.Errorf("An error occurred while initiating device auth: %v", err) } deviceAuthResp := deviceAuthConfig.DeviceAuthResponse @@ -200,8 +217,7 @@ func run(cmd *cobra.Command, argv []string) { r.Reporter.Infof("Checking status every %v seconds...", deviceAuthResp.Interval) token, err := deviceAuthConfig.PollForTokenExchange(ctx) if err != nil { - r.Reporter.Errorf("An error occurred while polling for token exchange: %v", err) - os.Exit(1) + return fmt.Errorf("An error occurred while polling for token exchange: %v", err) } args.token = token args.clientID = oauthClientId @@ -210,8 +226,7 @@ func run(cmd *cobra.Command, argv []string) { // Load the configuration file: cfg, err := config.Load() if err != nil { - r.Reporter.Errorf("Failed to load config file: %v", err) - os.Exit(1) + return fmt.Errorf("Failed to load config file: %v", err) } if cfg == nil { cfg = new(config.Config) @@ -238,7 +253,7 @@ func run(cmd *cobra.Command, argv []string) { fedramp.Disable() } - haveReqs := token != "" + haveReqs := token != "" || (args.clientID != "" && args.clientSecret != "") // Verify environment variables: if !haveReqs && !reAttempt && !fedramp.Enabled() { @@ -253,8 +268,7 @@ func run(cmd *cobra.Command, argv []string) { if !haveReqs { armed, err := cfg.Armed() if err != nil { - r.Reporter.Errorf("Failed to verify configuration: %v", err) - os.Exit(1) + return fmt.Errorf("Failed to verify configuration: %v", err) } haveReqs = armed } @@ -267,15 +281,13 @@ func run(cmd *cobra.Command, argv []string) { Required: true, }) if err != nil { - r.Reporter.Errorf("Failed to parse token: %v", err) - os.Exit(1) + return fmt.Errorf("Failed to parse token: %v", err) } haveReqs = token != "" } if !haveReqs { - r.Reporter.Errorf("Failed to login to OCM. See 'rosa login --help' for information.") - os.Exit(1) + return fmt.Errorf("Failed to login to OCM. See 'rosa login --help' for information.") } // Red Hat SSO does not issue encrypted refresh tokens, but AWS Cognito does. If the token @@ -345,15 +357,13 @@ func run(cmd *cobra.Command, argv []string) { // If a token has been provided parse it: jwtToken, err := config.ParseToken(token) if err != nil { - r.Reporter.Errorf("Failed to parse token: %v", err) - os.Exit(1) + return fmt.Errorf("Failed to parse token: %v", err) } // Put the token in the place of the configuration that corresponds to its type: typ, err := tokenType(jwtToken) if err != nil { - r.Reporter.Errorf("Failed to extract type from 'typ' claim of token: %v", err) - os.Exit(1) + return fmt.Errorf("Failed to extract type from 'typ' claim of token: %v", err) } switch typ { case "Bearer", "": @@ -363,8 +373,7 @@ func run(cmd *cobra.Command, argv []string) { cfg.AccessToken = "" cfg.RefreshToken = token default: - r.Reporter.Errorf("Don't know how to handle token type '%s' in token", typ) - os.Exit(1) + return fmt.Errorf("Don't know how to handle token type '%s' in token", typ) } } } @@ -377,19 +386,17 @@ func run(cmd *cobra.Command, argv []string) { if err != nil { if strings.Contains(err.Error(), "token needs to be updated") && !reAttempt { reattemptLogin(cmd, argv) - return + return nil } else { - r.Reporter.Errorf("Failed to create OCM connection: %v", err) - os.Exit(1) + return fmt.Errorf("Failed to create OCM connection: %v", err) } } - defer r.Cleanup() accessToken, refreshToken, err := r.OCMClient.GetConnectionTokens() if err != nil { - r.Reporter.Errorf("Failed to get token. Your session might be expired: %v", err) - r.Reporter.Infof("Get a new offline access token at %s", uiTokenPage) - os.Exit(1) + return fmt.Errorf( + "Failed to get token. Your session might be expired: %v\nGet a new offline access token at %s", + err, uiTokenPage) } reAttempt = false // Save the configuration: @@ -397,14 +404,15 @@ func run(cmd *cobra.Command, argv []string) { cfg.RefreshToken = refreshToken err = config.Save(cfg) if err != nil { - r.Reporter.Errorf("Failed to save config file: %v", err) - os.Exit(1) + return fmt.Errorf("Failed to save config file: %v", err) } - username, err := cfg.GetData("username") + username, err := cfg.GetData("preferred_username") if err != nil { - r.Reporter.Errorf("Failed to get username: %v", err) - os.Exit(1) + username, err = cfg.GetData("username") + if err != nil { + return fmt.Errorf("Failed to get username: %v", err) + } } r.Reporter.Infof("Logged in as '%s' on '%s'", username, cfg.URL) @@ -417,14 +425,15 @@ func run(cmd *cobra.Command, argv []string) { if args.useAuthCode || args.useDeviceCode { ssoURL, err := url.Parse(cfg.TokenURL) if err != nil { - r.Reporter.Errorf("can't parse token url '%s': %v", args.tokenURL, err) - os.Exit(1) + return fmt.Errorf("can't parse token url '%s': %v", args.tokenURL, err) } ssoHost := ssoURL.Scheme + "://" + ssoURL.Hostname() r.Reporter.Infof("To switch accounts, logout from %s and run `rosa logout` "+ "before attempting to login again", ssoHost) } + + return nil } func reattemptLogin(cmd *cobra.Command, argv []string) { @@ -485,9 +494,12 @@ func Call(cmd *cobra.Command, argv []string, reporter *rprtr.Object) error { } if isLoggedIn { - username, err := cfg.GetData("username") + username, err := cfg.GetData("preferred_username") if err != nil { - return fmt.Errorf("Failed to get username: %v", err) + username, err = cfg.GetData("username") + if err != nil { + return fmt.Errorf("Failed to get username: %v", err) + } } if reporter.IsTerminal() { diff --git a/cmd/login/cmd_test.go b/cmd/login/cmd_test.go new file mode 100644 index 0000000000..a4f7616c52 --- /dev/null +++ b/cmd/login/cmd_test.go @@ -0,0 +1,122 @@ +/* +Copyright (c) 2024 Red Hat, Inc. + +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 login + +import ( + "net/http" + "os" + "testing" + + . "github.com/onsi/ginkgo/v2" // nolint + . "github.com/onsi/gomega" // nolint + . "github.com/openshift-online/ocm-sdk-go/testing" + + "github.com/openshift/rosa/pkg/config" + "github.com/openshift/rosa/pkg/properties" + "github.com/openshift/rosa/pkg/test" +) + +func TestLoginCommand(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "rosa login command") +} + +var _ = Describe("Login", Ordered, func() { + var testRuntime test.TestingRuntime + var tmpdir string + invalidKeyring := "not-a-keyring" + + BeforeAll(func() { + tmpdir, _ = os.MkdirTemp("/tmp", ".ocm-config-*") + os.Setenv("OCM_CONFIG", tmpdir+"/ocm_config.json") + }) + + AfterAll(func() { + os.Setenv("OCM_CONFIG", "") + }) + + BeforeEach(func() { + testRuntime.InitRuntime() + }) + + When("Using offline token", func() { + It("Creates the configuration file", func() { + // Create the token: + claims := MakeClaims() + claims["username"] = "test" + accessTokenObj := MakeTokenObject(claims) + + // Run the command: + args.token = accessTokenObj.Raw + args.tokenURL = testRuntime.SsoServer.URL() + _, _, err := test.RunWithOutputCaptureAndArgv(runWithRuntime, testRuntime.RosaRuntime, Cmd, &[]string{}) + Expect(err).To(BeNil()) + + cfg, _ := config.Load() + Expect(cfg.AccessToken).To(Equal(accessTokenObj.Raw)) + Expect(cfg.TokenURL).To(Equal(testRuntime.SsoServer.URL())) + }) + }) + + When("Using client credentials grant", func() { + It("Creates the configuration file", func() { + // Run the command: + args.clientID = "my-client" + args.clientSecret = "my-secret" + args.tokenURL = testRuntime.SsoServer.URL() + args.env = testRuntime.ApiServer.URL() + + // Create the token: + claims := MakeClaims() + claims["username"] = "test" + accessTokenObj := MakeTokenObject(claims) + + testRuntime.SsoServer.AppendHandlers( + RespondWithAccessToken(accessTokenObj.Raw), + ) + + testRuntime.SsoServer.AppendHandlers(RespondWithJSON(http.StatusOK, "")) + + _, _, err := test.RunWithOutputCaptureAndArgv(runWithRuntime, testRuntime.RosaRuntime, Cmd, &[]string{}) + Expect(err).To(BeNil()) + + cfg, _ := config.Load() + Expect(cfg.AccessToken).ToNot(BeEmpty()) + Expect(cfg.TokenURL).To(Equal(testRuntime.SsoServer.URL())) + Expect(cfg.ClientID).To(Equal("my-client")) + Expect(cfg.ClientSecret).To(Equal("my-secret")) + }) + }) + + When(properties.KeyringEnvKey+" is used", func() { + AfterEach(func() { + // reset keyring + os.Setenv(properties.KeyringEnvKey, "") + }) + + It("Fails for an invalid keyring", func() { + os.Setenv(properties.KeyringEnvKey, invalidKeyring) + + // Run the command: + stdout, _, err := test.RunWithOutputCaptureAndArgv(runWithRuntime, testRuntime.RosaRuntime, Cmd, &[]string{}) + + Expect(err).ToNot(BeNil()) + Expect(stdout).To(BeEmpty()) + Expect(err.Error()).To(ContainSubstring("keyring is invalid")) + }) + }) +}) diff --git a/go.mod b/go.mod index 076c251276..462ec736c6 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,9 @@ require ( ) require ( + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/99designs/keyring v1.2.2 // indirect + github.com/alessio/shellescape v1.4.1 // indirect github.com/andybalholm/cascadia v1.3.2 // indirect github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect github.com/aws/smithy-go v1.19.0 // indirect @@ -41,14 +44,19 @@ require ( github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect + github.com/dvsekhvalnov/jose2go v1.6.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fatih/color v1.7.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/gorilla/css v1.0.1 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/itchyny/gojq v0.12.7 // indirect github.com/itchyny/timefmt-go v0.1.3 // indirect @@ -69,12 +77,14 @@ require ( github.com/microcosm-cc/bluemonday v1.0.26 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mtibben/percent v0.2.1 // indirect github.com/prometheus/client_golang v1.18.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.46.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect + github.com/zalando/go-keyring v0.2.3 // indirect golang.org/x/crypto v0.18.0 // indirect golang.org/x/net v0.20.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect diff --git a/go.sum b/go.sum index 5310edac6c..e3ebb3f3aa 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= +github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= github.com/AlecAivazis/survey/v2 v2.2.15 h1:6UNMnk+YGegYFiPfdTOyZDIN+m08x2nGnqOn15BWcEQ= github.com/AlecAivazis/survey/v2 v2.2.15/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -7,6 +11,8 @@ github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nB github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= @@ -39,6 +45,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHH github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -46,6 +54,8 @@ github.com/dchest/validator v0.0.0-20191217151620-8e45250f2371 h1:BuLreR1acrosGs github.com/dchest/validator v0.0.0-20191217151620-8e45250f2371/go.mod h1:ZfpgrLR1i3mQWz5fIRfkyMIh9zLOy3MwTc7hUBVPlww= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= +github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= @@ -59,6 +69,10 @@ github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbV github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -85,6 +99,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174 h1:WlZsjVhE8Af9IcZDGgJGQpNflI3+MJSBhsgT5PCtzBQ= @@ -196,8 +212,11 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/nathan-fiscaletti/consolesize-go v0.0.0-20210105204122-a87d9f614b9d h1:PQW4Aqovdqc9efHl9EVA+bhKmuZ4ME1HvSYYDvaDiK0= github.com/nathan-fiscaletti/consolesize-go v0.0.0-20210105204122-a87d9f614b9d/go.mod h1:cxIIfNMTwff8f/ZvRouvWYF6wOoO7nj99neWSx2q/Es= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= @@ -246,6 +265,8 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -259,6 +280,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= +github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zgalor/weberr v0.6.0 h1:k6XSpFcOUNco8qtyAMBqXbCAVUivV7mRxGE5CMqHHdM= github.com/zgalor/weberr v0.6.0/go.mod h1:cqK89mj84q3PRgqQXQFWJDzCorOd8xOtov/ulOnqDwc= @@ -400,6 +423,7 @@ google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7 google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= diff --git a/pkg/config/config.go b/pkg/config/config.go index 8633d21584..15f0d74559 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -29,8 +29,16 @@ import ( "github.com/golang-jwt/jwt/v4" "github.com/golang/glog" sdk "github.com/openshift-online/ocm-sdk-go" + "github.com/openshift-online/ocm-sdk-go/authentication/securestore" "github.com/openshift/rosa/pkg/debug" + "github.com/openshift/rosa/pkg/properties" +) + +var ( + UpsertConfigToKeyring = securestore.UpsertConfigToKeyring + RemoveConfigFromKeyring = securestore.RemoveConfigFromKeyring + GetConfigFromKeyring = securestore.GetConfigFromKeyring ) // Config is the type used to store the configuration of the client. @@ -46,9 +54,39 @@ type Config struct { FedRAMP bool `json:"fedramp,omitempty" doc:"Indicates FedRAMP."` } -// Load loads the configuration from the configuration file. If the configuration file doesn't exist -// it will return an empty configuration object. +// Loads the configuration from the OS keyring if requested, load from the configuration file if not func Load() (cfg *Config, err error) { + if keyring, ok := IsKeyringManaged(); ok { + return loadFromOS(keyring) + } + + return loadFromFile() +} + +// Loads the configuration from the OS keyring. If the configuration doesn't exist +// it will return an empty configuration object. +func loadFromOS(keyring string) (cfg *Config, err error) { + cfg = &Config{} + + data, err := GetConfigFromKeyring(keyring) + if err != nil { + return nil, fmt.Errorf("can't load config from OS keyring [%s]: %v", keyring, err) + } + // No config found, return + if len(data) == 0 { + return nil, nil + } + err = json.Unmarshal(data, cfg) + if err != nil { + // Treat the config as empty if it can't be unmarshalled, it is invalid + return nil, nil + } + return cfg, nil +} + +// Loads the configuration from the configuration file. If the configuration file doesn't exist +// it will return an empty configuration object. +func loadFromFile() (cfg *Config, err error) { file, err := Location() if err != nil { return @@ -84,15 +122,25 @@ func Save(cfg *Config) error { if err != nil { return err } + + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return fmt.Errorf("can't marshal config: %v", err) + } + + if keyring, ok := IsKeyringManaged(); ok { + err := UpsertConfigToKeyring(keyring, data) + if err != nil { + return fmt.Errorf("can't save config to OS keyring [%s]: %v", keyring, err) + } + return nil + } + dir := filepath.Dir(file) err = os.MkdirAll(dir, os.FileMode(0755)) if err != nil { return fmt.Errorf("Failed to create directory %s: %v", dir, err) } - data, err := json.MarshalIndent(cfg, "", " ") - if err != nil { - return fmt.Errorf("Failed to marshal config: %v", err) - } err = os.WriteFile(file, data, 0600) if err != nil { return fmt.Errorf("Failed to write file '%s': %v", file, err) @@ -102,6 +150,14 @@ func Save(cfg *Config) error { // Remove removes the configuration file. func Remove() error { + if keyring, ok := IsKeyringManaged(); ok { + err := RemoveConfigFromKeyring(keyring) + if err != nil { + return fmt.Errorf("can't remove configuration from keyring [%s]: %w", keyring, err) + } + return nil + } + file, err := Location() if err != nil { return err @@ -284,3 +340,9 @@ func (c *Config) Connection() (connection *sdk.Connection, err error) { return } + +// IsKeyringManaged returns the keyring name and a boolean indicating if the config is managed by the keyring. +func IsKeyringManaged() (keyring string, ok bool) { + keyring = os.Getenv(properties.KeyringEnvKey) + return keyring, keyring != "" +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000000..f9cec360d0 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,200 @@ +/* +Copyright (c) 2024 Red Hat, Inc. + +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 config + +import ( + "encoding/json" + "fmt" + "os" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/openshift/rosa/pkg/properties" +) + +type mockSpy struct { + calledUpsert bool + calledRemove bool + calledGet bool + testCfg []byte + upsertErr error + removeErr error + getErr error +} + +func (m *mockSpy) MockUpsertConfigToKeyring(keyring string, data []byte) error { + m.calledUpsert = true + return m.upsertErr +} + +func (m *mockSpy) MockRemoveConfigFromKeyring(keyring string) error { + m.calledRemove = true + return m.removeErr +} + +func (m *mockSpy) MockGetConfigFromKeyring(keyring string) ([]byte, error) { + m.calledGet = true + return m.testCfg, m.getErr +} + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Config Suite") +} + +var _ = Describe("Config", func() { + When("Load()", func() { + Context(properties.KeyringEnvKey+" is set", func() { + BeforeEach(func() { + os.Setenv(properties.KeyringEnvKey, "keyring") + }) + + AfterEach(func() { + os.Setenv(properties.KeyringEnvKey, "") + }) + + It("Returns a valid config", func() { + data := generateConfigBytes(Config{ + AccessToken: "access_token", + }) + mockSpy := &mockSpy{testCfg: data} + GetConfigFromKeyring = mockSpy.MockGetConfigFromKeyring + + cfg, err := Load() + Expect(err).To(BeNil()) + Expect(cfg).ToNot(BeNil()) + Expect(cfg.AccessToken).To(Equal("access_token")) + Expect(mockSpy.calledGet).To(BeTrue()) + }) + + It("Returns nil for no config content", func() { + mockSpy := &mockSpy{testCfg: nil} + GetConfigFromKeyring = mockSpy.MockGetConfigFromKeyring + + cfg, err := Load() + Expect(err).To(BeNil()) + Expect(cfg).To(BeNil()) + Expect(mockSpy.calledGet).To(BeTrue()) + }) + + It("Returns nil for invalid config content", func() { + data := generateInvalidConfigBytes() + mockSpy := &mockSpy{testCfg: data} + GetConfigFromKeyring = mockSpy.MockGetConfigFromKeyring + + cfg, err := Load() + Expect(err).To(BeNil()) + Expect(cfg).To(BeNil()) + Expect(mockSpy.calledGet).To(BeTrue()) + }) + + It("Handles Error", func() { + data := generateInvalidConfigBytes() + mockSpy := &mockSpy{testCfg: data} + mockSpy.getErr = fmt.Errorf("error") + GetConfigFromKeyring = mockSpy.MockGetConfigFromKeyring + + cfg, err := Load() + Expect(err).NotTo(BeNil()) + Expect(cfg).To(BeNil()) + Expect(mockSpy.calledGet).To(BeTrue()) + }) + }) + }) + + When("Save()", func() { + Context(properties.KeyringEnvKey+" is set", func() { + BeforeEach(func() { + os.Setenv(properties.KeyringEnvKey, "keyring") + }) + + AfterEach(func() { + os.Setenv(properties.KeyringEnvKey, "") + }) + + It("Saves a valid config", func() { + data := &Config{ + AccessToken: "access_token", + } + mockSpy := &mockSpy{} + UpsertConfigToKeyring = mockSpy.MockUpsertConfigToKeyring + + err := Save(data) + Expect(err).To(BeNil()) + Expect(mockSpy.calledUpsert).To(BeTrue()) + }) + + It("Handles Error", func() { + data := &Config{ + AccessToken: "access_token", + } + mockSpy := &mockSpy{} + mockSpy.upsertErr = fmt.Errorf("error") + UpsertConfigToKeyring = mockSpy.MockUpsertConfigToKeyring + + err := Save(data) + Expect(err).NotTo(BeNil()) + Expect(mockSpy.calledUpsert).To(BeTrue()) + }) + }) + }) + + When("Remove()", func() { + Context(properties.KeyringEnvKey+" is set", func() { + BeforeEach(func() { + os.Setenv(properties.KeyringEnvKey, "keyring") + }) + + AfterEach(func() { + os.Setenv(properties.KeyringEnvKey, "") + }) + + It("Removes a config", func() { + mockSpy := &mockSpy{} + RemoveConfigFromKeyring = mockSpy.MockRemoveConfigFromKeyring + + err := Remove() + Expect(err).To(BeNil()) + Expect(mockSpy.calledRemove).To(BeTrue()) + }) + + It("Handles Error", func() { + mockSpy := &mockSpy{} + mockSpy.removeErr = fmt.Errorf("error") + RemoveConfigFromKeyring = mockSpy.MockRemoveConfigFromKeyring + + err := Remove() + Expect(err).NotTo(BeNil()) + Expect(mockSpy.calledRemove).To(BeTrue()) + }) + }) + }) +}) + +func generateInvalidConfigBytes() []byte { + return []byte("foo") +} + +func generateConfigBytes(config Config) []byte { + data := &config + jsonData, err := json.Marshal(data) + Expect(err).To(BeNil()) + + return jsonData +} diff --git a/pkg/properties/properties.go b/pkg/properties/properties.go index 5bc463ccc1..3906f9e783 100644 --- a/pkg/properties/properties.go +++ b/pkg/properties/properties.go @@ -30,3 +30,5 @@ const FakeCluster = "fake_cluster" const UseLocalCredentials = "use_local_credentials" const ProvisionShardId = "provision_shard_id" + +const KeyringEnvKey = "OCM_KEYRING" diff --git a/pkg/test/helpers.go b/pkg/test/helpers.go index acf5f647ca..7e796152b6 100644 --- a/pkg/test/helpers.go +++ b/pkg/test/helpers.go @@ -7,7 +7,6 @@ import ( "net/http" "os" "reflect" - "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -206,7 +205,10 @@ func (t *TestingRuntime) InitRuntime() { t.ApiServer.SetUnhandledRequestStatusCode(http.StatusInternalServerError) // Create the token: - accessToken := MakeTokenString("Bearer", 15*time.Minute) + claims := MakeClaims() + claims["username"] = "foo" + accessTokenObj := MakeTokenObject(claims) + accessToken := accessTokenObj.Raw // Prepare the server: t.SsoServer.AppendHandlers( diff --git a/vendor/github.com/99designs/go-keychain/.gitignore b/vendor/github.com/99designs/go-keychain/.gitignore new file mode 100644 index 0000000000..daf913b1b3 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/99designs/go-keychain/.golangci.yml b/vendor/github.com/99designs/go-keychain/.golangci.yml new file mode 100644 index 0000000000..23aaf4325b --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/.golangci.yml @@ -0,0 +1,11 @@ +linters-settings: + gocritic: + disabled-checks: + - ifElseChain + - elseif + +linters: + enable: + - gofmt + - gocritic + - unconvert diff --git a/vendor/github.com/99designs/go-keychain/.travis.yml b/vendor/github.com/99designs/go-keychain/.travis.yml new file mode 100644 index 0000000000..2fba2395cd --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/.travis.yml @@ -0,0 +1,20 @@ +language: go + +os: + - osx + - linux + +before_install: + - go get golang.org/x/lint/golint + +script: + - go vet ./... + - golint ./... + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.16.0 + - golangci-lint run + - go test -tags skipsecretserviceintegrationtests ./... + +go: + - 1.10.x + - 1.11.x + - 1.12.x diff --git a/vendor/github.com/99designs/go-keychain/LICENSE b/vendor/github.com/99designs/go-keychain/LICENSE new file mode 100644 index 0000000000..2d54c6560e --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Keybase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/99designs/go-keychain/README.md b/vendor/github.com/99designs/go-keychain/README.md new file mode 100644 index 0000000000..4a9eeb24b6 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/README.md @@ -0,0 +1,159 @@ +# Go Keychain + +[![Travis CI](https://travis-ci.org/keybase/go-keychain.svg?branch=master)](https://travis-ci.org/keybase/go-keychain) + +A library for accessing the Keychain for macOS, iOS, and Linux in Go (golang). + +Requires macOS 10.9 or greater and iOS 8 or greater. On Linux, communicates to +a provider of the DBUS SecretService spec like gnome-keyring or ksecretservice. + +```go +import "github.com/keybase/go-keychain" +``` + + +## Mac/iOS Usage + +The API is meant to mirror the macOS/iOS Keychain API and is not necessarily idiomatic go. + +#### Add Item + +```go +item := keychain.NewItem() +item.SetSecClass(keychain.SecClassGenericPassword) +item.SetService("MyService") +item.SetAccount("gabriel") +item.SetLabel("A label") +item.SetAccessGroup("A123456789.group.com.mycorp") +item.SetData([]byte("toomanysecrets")) +item.SetSynchronizable(keychain.SynchronizableNo) +item.SetAccessible(keychain.AccessibleWhenUnlocked) +err := keychain.AddItem(item) + +if err == keychain.ErrorDuplicateItem { + // Duplicate +} +``` + +#### Query Item + +Query for multiple results, returning attributes: + +```go +query := keychain.NewItem() +query.SetSecClass(keychain.SecClassGenericPassword) +query.SetService(service) +query.SetAccount(account) +query.SetAccessGroup(accessGroup) +query.SetMatchLimit(keychain.MatchLimitAll) +query.SetReturnAttributes(true) +results, err := keychain.QueryItem(query) +if err != nil { + // Error +} else { + for _, r := range results { + fmt.Printf("%#v\n", r) + } +} +``` + +Query for a single result, returning data: + +```go +query := keychain.NewItem() +query.SetSecClass(keychain.SecClassGenericPassword) +query.SetService(service) +query.SetAccount(account) +query.SetAccessGroup(accessGroup) +query.SetMatchLimit(keychain.MatchLimitOne) +query.SetReturnData(true) +results, err := keychain.QueryItem(query) +if err != nil { + // Error +} else if len(results) != 1 { + // Not found +} else { + password := string(results[0].Data) +} +``` + +#### Delete Item + +Delete a generic password item with service and account: + +```go +item := keychain.NewItem() +item.SetSecClass(keychain.SecClassGenericPassword) +item.SetService(service) +item.SetAccount(account) +err := keychain.DeleteItem(item) +``` + +### Other + +There are some convenience methods for generic password: + +```go +// Create generic password item with service, account, label, password, access group +item := keychain.NewGenericPassword("MyService", "gabriel", "A label", []byte("toomanysecrets"), "A123456789.group.com.mycorp") +item.SetSynchronizable(keychain.SynchronizableNo) +item.SetAccessible(keychain.AccessibleWhenUnlocked) +err := keychain.AddItem(item) +if err == keychain.ErrorDuplicateItem { + // Duplicate +} + +accounts, err := keychain.GetGenericPasswordAccounts("MyService") +// Should have 1 account == "gabriel" + +err := keychain.DeleteGenericPasswordItem("MyService", "gabriel") +if err == keychain.ErrorNotFound { + // Not found +} +``` + +### OS X + +Creating a new keychain and add an item to it: + +```go + +// Add a new key chain into ~/Application Support/Keychains, with the provided password +k, err := keychain.NewKeychain("mykeychain.keychain", "my keychain password") +if err != nil { + // Error creating +} + +// Create generic password item with service, account, label, password, access group +item := keychain.NewGenericPassword("MyService", "gabriel", "A label", []byte("toomanysecrets"), "A123456789.group.com.mycorp") +item.UseKeychain(k) +err := keychain.AddItem(item) +if err != nil { + // Error creating +} +``` + +Using a Keychain at path: + +```go +k, err := keychain.NewWithPath("mykeychain.keychain") +``` + +Set a trusted applications for item (OS X only): + +```go +item := keychain.NewGenericPassword("MyService", "gabriel", "A label", []byte("toomanysecrets"), "A123456789.group.com.mycorp") +trustedApplications := []string{"/Applications/Mail.app"} +item.SetAccess(&keychain.Access{Label: "Mail", TrustedApplications: trustedApplications}) +err := keychain.AddItem(item) +``` + +## iOS + +Bindable package in `bind`. iOS project in `ios`. Run that project to test iOS. + +To re-generate framework: + +``` +(cd bind && gomobile bind -target=ios -tags=ios -o ../ios/bind.framework) +``` diff --git a/vendor/github.com/99designs/go-keychain/corefoundation.go b/vendor/github.com/99designs/go-keychain/corefoundation.go new file mode 100644 index 0000000000..c45ee7a842 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/corefoundation.go @@ -0,0 +1,359 @@ +// +build darwin ios + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation + +#include + +// Can't cast a *uintptr to *unsafe.Pointer in Go, and casting +// C.CFTypeRef to unsafe.Pointer is unsafe in Go, so have shim functions to +// do the casting in C (where it's safe). + +// We add a suffix to the C functions below, because we copied this +// file from go-kext, which means that any project that depends on this +// package and go-kext would run into duplicate symbol errors otherwise. +// +// TODO: Move this file into its own package depended on by go-kext +// and this package. + +CFDictionaryRef CFDictionaryCreateSafe2(CFAllocatorRef allocator, const uintptr_t *keys, const uintptr_t *values, CFIndex numValues, const CFDictionaryKeyCallBacks *keyCallBacks, const CFDictionaryValueCallBacks *valueCallBacks) { + return CFDictionaryCreate(allocator, (const void **)keys, (const void **)values, numValues, keyCallBacks, valueCallBacks); +} + +CFArrayRef CFArrayCreateSafe2(CFAllocatorRef allocator, const uintptr_t *values, CFIndex numValues, const CFArrayCallBacks *callBacks) { + return CFArrayCreate(allocator, (const void **)values, numValues, callBacks); +} +*/ +import "C" +import ( + "errors" + "fmt" + "math" + "reflect" + "unicode/utf8" + "unsafe" +) + +// Release releases memory pointed to by a CFTypeRef. +func Release(ref C.CFTypeRef) { + C.CFRelease(ref) +} + +// BytesToCFData will return a CFDataRef and if non-nil, must be released with +// Release(ref). +func BytesToCFData(b []byte) (C.CFDataRef, error) { + if uint64(len(b)) > math.MaxUint32 { + return 0, errors.New("Data is too large") + } + var p *C.UInt8 + if len(b) > 0 { + p = (*C.UInt8)(&b[0]) + } + cfData := C.CFDataCreate(C.kCFAllocatorDefault, p, C.CFIndex(len(b))) + if cfData == 0 { + return 0, fmt.Errorf("CFDataCreate failed") + } + return cfData, nil +} + +// CFDataToBytes converts CFData to bytes. +func CFDataToBytes(cfData C.CFDataRef) ([]byte, error) { + return C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(cfData)), C.int(C.CFDataGetLength(cfData))), nil +} + +// MapToCFDictionary will return a CFDictionaryRef and if non-nil, must be +// released with Release(ref). +func MapToCFDictionary(m map[C.CFTypeRef]C.CFTypeRef) (C.CFDictionaryRef, error) { + var keys, values []C.uintptr_t + for key, value := range m { + keys = append(keys, C.uintptr_t(key)) + values = append(values, C.uintptr_t(value)) + } + numValues := len(values) + var keysPointer, valuesPointer *C.uintptr_t + if numValues > 0 { + keysPointer = &keys[0] + valuesPointer = &values[0] + } + cfDict := C.CFDictionaryCreateSafe2(C.kCFAllocatorDefault, keysPointer, valuesPointer, C.CFIndex(numValues), + &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) //nolint + if cfDict == 0 { + return 0, fmt.Errorf("CFDictionaryCreate failed") + } + return cfDict, nil +} + +// CFDictionaryToMap converts CFDictionaryRef to a map. +func CFDictionaryToMap(cfDict C.CFDictionaryRef) (m map[C.CFTypeRef]C.CFTypeRef) { + count := C.CFDictionaryGetCount(cfDict) + if count > 0 { + keys := make([]C.CFTypeRef, count) + values := make([]C.CFTypeRef, count) + C.CFDictionaryGetKeysAndValues(cfDict, (*unsafe.Pointer)(unsafe.Pointer(&keys[0])), (*unsafe.Pointer)(unsafe.Pointer(&values[0]))) + m = make(map[C.CFTypeRef]C.CFTypeRef, count) + for i := C.CFIndex(0); i < count; i++ { + m[keys[i]] = values[i] + } + } + return +} + +// StringToCFString will return a CFStringRef and if non-nil, must be released with +// Release(ref). +func StringToCFString(s string) (C.CFStringRef, error) { + if !utf8.ValidString(s) { + return 0, errors.New("Invalid UTF-8 string") + } + if uint64(len(s)) > math.MaxUint32 { + return 0, errors.New("String is too large") + } + + bytes := []byte(s) + var p *C.UInt8 + if len(bytes) > 0 { + p = (*C.UInt8)(&bytes[0]) + } + return C.CFStringCreateWithBytes(C.kCFAllocatorDefault, p, C.CFIndex(len(s)), C.kCFStringEncodingUTF8, C.false), nil +} + +// CFStringToString converts a CFStringRef to a string. +func CFStringToString(s C.CFStringRef) string { + p := C.CFStringGetCStringPtr(s, C.kCFStringEncodingUTF8) + if p != nil { + return C.GoString(p) + } + length := C.CFStringGetLength(s) + if length == 0 { + return "" + } + maxBufLen := C.CFStringGetMaximumSizeForEncoding(length, C.kCFStringEncodingUTF8) + if maxBufLen == 0 { + return "" + } + buf := make([]byte, maxBufLen) + var usedBufLen C.CFIndex + _ = C.CFStringGetBytes(s, C.CFRange{0, length}, C.kCFStringEncodingUTF8, C.UInt8(0), C.false, (*C.UInt8)(&buf[0]), maxBufLen, &usedBufLen) + return string(buf[:usedBufLen]) +} + +// ArrayToCFArray will return a CFArrayRef and if non-nil, must be released with +// Release(ref). +func ArrayToCFArray(a []C.CFTypeRef) C.CFArrayRef { + var values []C.uintptr_t + for _, value := range a { + values = append(values, C.uintptr_t(value)) + } + numValues := len(values) + var valuesPointer *C.uintptr_t + if numValues > 0 { + valuesPointer = &values[0] + } + return C.CFArrayCreateSafe2(C.kCFAllocatorDefault, valuesPointer, C.CFIndex(numValues), &C.kCFTypeArrayCallBacks) //nolint +} + +// CFArrayToArray converts a CFArrayRef to an array of CFTypes. +func CFArrayToArray(cfArray C.CFArrayRef) (a []C.CFTypeRef) { + count := C.CFArrayGetCount(cfArray) + if count > 0 { + a = make([]C.CFTypeRef, count) + C.CFArrayGetValues(cfArray, C.CFRange{0, count}, (*unsafe.Pointer)(unsafe.Pointer(&a[0]))) + } + return +} + +// Convertable knows how to convert an instance to a CFTypeRef. +type Convertable interface { + Convert() (C.CFTypeRef, error) +} + +// ConvertMapToCFDictionary converts a map to a CFDictionary and if non-nil, +// must be released with Release(ref). +func ConvertMapToCFDictionary(attr map[string]interface{}) (C.CFDictionaryRef, error) { + m := make(map[C.CFTypeRef]C.CFTypeRef) + for key, i := range attr { + var valueRef C.CFTypeRef + switch val := i.(type) { + default: + return 0, fmt.Errorf("Unsupported value type: %v", reflect.TypeOf(i)) + case C.CFTypeRef: + valueRef = val + case bool: + if val { + valueRef = C.CFTypeRef(C.kCFBooleanTrue) + } else { + valueRef = C.CFTypeRef(C.kCFBooleanFalse) + } + case []byte: + bytesRef, err := BytesToCFData(val) + if err != nil { + return 0, err + } + valueRef = C.CFTypeRef(bytesRef) + defer Release(valueRef) + case string: + stringRef, err := StringToCFString(val) + if err != nil { + return 0, err + } + valueRef = C.CFTypeRef(stringRef) + defer Release(valueRef) + case Convertable: + convertedRef, err := val.Convert() + if err != nil { + return 0, err + } + valueRef = convertedRef + defer Release(valueRef) + } + keyRef, err := StringToCFString(key) + if err != nil { + return 0, err + } + m[C.CFTypeRef(keyRef)] = valueRef + defer Release(C.CFTypeRef(keyRef)) + } + + cfDict, err := MapToCFDictionary(m) + if err != nil { + return 0, err + } + return cfDict, nil +} + +// CFTypeDescription returns type string for CFTypeRef. +func CFTypeDescription(ref C.CFTypeRef) string { + typeID := C.CFGetTypeID(ref) + typeDesc := C.CFCopyTypeIDDescription(typeID) + defer Release(C.CFTypeRef(typeDesc)) + return CFStringToString(typeDesc) +} + +// Convert converts a CFTypeRef to a go instance. +func Convert(ref C.CFTypeRef) (interface{}, error) { + typeID := C.CFGetTypeID(ref) + if typeID == C.CFStringGetTypeID() { + return CFStringToString(C.CFStringRef(ref)), nil + } else if typeID == C.CFDictionaryGetTypeID() { + return ConvertCFDictionary(C.CFDictionaryRef(ref)) + } else if typeID == C.CFArrayGetTypeID() { + arr := CFArrayToArray(C.CFArrayRef(ref)) + results := make([]interface{}, 0, len(arr)) + for _, ref := range arr { + v, err := Convert(ref) + if err != nil { + return nil, err + } + results = append(results, v) + } + return results, nil + } else if typeID == C.CFDataGetTypeID() { + b, err := CFDataToBytes(C.CFDataRef(ref)) + if err != nil { + return nil, err + } + return b, nil + } else if typeID == C.CFNumberGetTypeID() { + return CFNumberToInterface(C.CFNumberRef(ref)), nil + } else if typeID == C.CFBooleanGetTypeID() { + if C.CFBooleanGetValue(C.CFBooleanRef(ref)) != 0 { + return true, nil + } + return false, nil + } + + return nil, fmt.Errorf("Invalid type: %s", CFTypeDescription(ref)) +} + +// ConvertCFDictionary converts a CFDictionary to map (deep). +func ConvertCFDictionary(d C.CFDictionaryRef) (map[interface{}]interface{}, error) { + m := CFDictionaryToMap(d) + result := make(map[interface{}]interface{}) + + for k, v := range m { + gk, err := Convert(k) + if err != nil { + return nil, err + } + gv, err := Convert(v) + if err != nil { + return nil, err + } + result[gk] = gv + } + return result, nil +} + +// CFNumberToInterface converts the CFNumberRef to the most appropriate numeric +// type. +// This code is from github.com/kballard/go-osx-plist. +func CFNumberToInterface(cfNumber C.CFNumberRef) interface{} { + typ := C.CFNumberGetType(cfNumber) + switch typ { + case C.kCFNumberSInt8Type: + var sint C.SInt8 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint + return int8(sint) + case C.kCFNumberSInt16Type: + var sint C.SInt16 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint + return int16(sint) + case C.kCFNumberSInt32Type: + var sint C.SInt32 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint + return int32(sint) + case C.kCFNumberSInt64Type: + var sint C.SInt64 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint + return int64(sint) + case C.kCFNumberFloat32Type: + var float C.Float32 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint + return float32(float) + case C.kCFNumberFloat64Type: + var float C.Float64 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint + return float64(float) + case C.kCFNumberCharType: + var char C.char + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&char)) //nolint + return byte(char) + case C.kCFNumberShortType: + var short C.short + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&short)) //nolint + return int16(short) + case C.kCFNumberIntType: + var i C.int + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&i)) //nolint + return int32(i) + case C.kCFNumberLongType: + var long C.long + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&long)) //nolint + return int(long) + case C.kCFNumberLongLongType: + // This is the only type that may actually overflow us + var longlong C.longlong + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&longlong)) //nolint + return int64(longlong) + case C.kCFNumberFloatType: + var float C.float + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint + return float32(float) + case C.kCFNumberDoubleType: + var double C.double + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&double)) //nolint + return float64(double) + case C.kCFNumberCFIndexType: + // CFIndex is a long + var index C.CFIndex + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&index)) //nolint + return int(index) + case C.kCFNumberNSIntegerType: + // We don't have a definition of NSInteger, but we know it's either an int or a long + var nsInt C.long + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&nsInt)) //nolint + return int(nsInt) + } + panic("Unknown CFNumber type") +} diff --git a/vendor/github.com/99designs/go-keychain/datetime.go b/vendor/github.com/99designs/go-keychain/datetime.go new file mode 100644 index 0000000000..8124a6ee6f --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/datetime.go @@ -0,0 +1,68 @@ +// +build darwin ios + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation + +#include +*/ +import "C" +import ( + "math" + "time" +) + +const nsPerSec = 1000 * 1000 * 1000 + +// absoluteTimeIntervalSince1970() returns the number of seconds from +// the Unix epoch (1970-01-01T00:00:00+00:00) to the Core Foundation +// absolute reference date (2001-01-01T00:00:00+00:00). It should be +// exactly 978307200. +func absoluteTimeIntervalSince1970() int64 { + return int64(C.kCFAbsoluteTimeIntervalSince1970) +} + +func unixToAbsoluteTime(s int64, ns int64) C.CFAbsoluteTime { + // Subtract as int64s first before converting to floating + // point to minimize precision loss (assuming the given time + // isn't much earlier than the Core Foundation absolute + // reference date). + abs := s - absoluteTimeIntervalSince1970() + return C.CFAbsoluteTime(abs) + C.CFTimeInterval(ns)/nsPerSec +} + +func absoluteTimeToUnix(abs C.CFAbsoluteTime) (int64, int64) { + int, frac := math.Modf(float64(abs)) + return int64(int) + absoluteTimeIntervalSince1970(), int64(frac * nsPerSec) +} + +// TimeToCFDate will convert the given time.Time to a CFDateRef, which +// must be released with Release(ref). +func TimeToCFDate(t time.Time) C.CFDateRef { + s := t.Unix() + ns := int64(t.Nanosecond()) + abs := unixToAbsoluteTime(s, ns) + return C.CFDateCreate(C.kCFAllocatorDefault, abs) +} + +// CFDateToTime will convert the given CFDateRef to a time.Time. +func CFDateToTime(d C.CFDateRef) time.Time { + abs := C.CFDateGetAbsoluteTime(d) + s, ns := absoluteTimeToUnix(abs) + return time.Unix(s, ns) +} + +// Wrappers around C functions for testing. + +func cfDateToAbsoluteTime(d C.CFDateRef) C.CFAbsoluteTime { + return C.CFDateGetAbsoluteTime(d) +} + +func absoluteTimeToCFDate(abs C.CFAbsoluteTime) C.CFDateRef { + return C.CFDateCreate(C.kCFAllocatorDefault, abs) +} + +func releaseCFDate(d C.CFDateRef) { + Release(C.CFTypeRef(d)) +} diff --git a/vendor/github.com/99designs/go-keychain/ios.go b/vendor/github.com/99designs/go-keychain/ios.go new file mode 100644 index 0000000000..abbaf285d6 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/ios.go @@ -0,0 +1,22 @@ +// +build darwin,ios + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation -framework Security + +#include +#include +*/ +import "C" + +var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible)) +var accessibleTypeRef = map[Accessible]C.CFTypeRef{ + AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked), + AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock), + AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways), + AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), + AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly), + AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), + AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly), +} diff --git a/vendor/github.com/99designs/go-keychain/keychain.go b/vendor/github.com/99designs/go-keychain/keychain.go new file mode 100644 index 0000000000..f5d02adf9c --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/keychain.go @@ -0,0 +1,531 @@ +// +build darwin + +package keychain + +// See https://developer.apple.com/library/ios/documentation/Security/Reference/keychainservices/index.html for the APIs used below. + +// Also see https://developer.apple.com/library/ios/documentation/Security/Conceptual/keychainServConcepts/01introduction/introduction.html . + +/* +#cgo LDFLAGS: -framework CoreFoundation -framework Security + +#include +#include +*/ +import "C" +import ( + "fmt" + "time" +) + +// Error defines keychain errors +type Error int + +var ( + // ErrorUnimplemented corresponds to errSecUnimplemented result code + ErrorUnimplemented = Error(C.errSecUnimplemented) + // ErrorParam corresponds to errSecParam result code + ErrorParam = Error(C.errSecParam) + // ErrorAllocate corresponds to errSecAllocate result code + ErrorAllocate = Error(C.errSecAllocate) + // ErrorNotAvailable corresponds to errSecNotAvailable result code + ErrorNotAvailable = Error(C.errSecNotAvailable) + // ErrorAuthFailed corresponds to errSecAuthFailed result code + ErrorAuthFailed = Error(C.errSecAuthFailed) + // ErrorDuplicateItem corresponds to errSecDuplicateItem result code + ErrorDuplicateItem = Error(C.errSecDuplicateItem) + // ErrorItemNotFound corresponds to errSecItemNotFound result code + ErrorItemNotFound = Error(C.errSecItemNotFound) + // ErrorInteractionNotAllowed corresponds to errSecInteractionNotAllowed result code + ErrorInteractionNotAllowed = Error(C.errSecInteractionNotAllowed) + // ErrorDecode corresponds to errSecDecode result code + ErrorDecode = Error(C.errSecDecode) + // ErrorNoSuchKeychain corresponds to errSecNoSuchKeychain result code + ErrorNoSuchKeychain = Error(C.errSecNoSuchKeychain) + // ErrorNoAcccessForItem corresponds to errSecNoAccessForItem result code + ErrorNoAccessForItem = Error(C.errSecNoAccessForItem) +) + +func checkError(errCode C.OSStatus) error { + if errCode == C.errSecSuccess { + return nil + } + return Error(errCode) +} + +func (k Error) Error() (msg string) { + // SecCopyErrorMessageString is only available on OSX, so derive manually. + // Messages derived from `$ security error $errcode`. + switch k { + case ErrorUnimplemented: + msg = "Function or operation not implemented." + case ErrorParam: + msg = "One or more parameters passed to the function were not valid." + case ErrorAllocate: + msg = "Failed to allocate memory." + case ErrorNotAvailable: + msg = "No keychain is available. You may need to restart your computer." + case ErrorAuthFailed: + msg = "The user name or passphrase you entered is not correct." + case ErrorDuplicateItem: + msg = "The specified item already exists in the keychain." + case ErrorItemNotFound: + msg = "The specified item could not be found in the keychain." + case ErrorInteractionNotAllowed: + msg = "User interaction is not allowed." + case ErrorDecode: + msg = "Unable to decode the provided data." + case ErrorNoSuchKeychain: + msg = "The specified keychain could not be found." + case ErrorNoAccessForItem: + msg = "The specified item has no access control." + default: + msg = "Keychain Error." + } + return fmt.Sprintf("%s (%d)", msg, k) +} + +// SecClass is the items class code +type SecClass int + +// Keychain Item Classes +var ( + /* + kSecClassGenericPassword item attributes: + kSecAttrAccess (OS X only) + kSecAttrAccessGroup (iOS; also OS X if kSecAttrSynchronizable specified) + kSecAttrAccessible (iOS; also OS X if kSecAttrSynchronizable specified) + kSecAttrAccount + kSecAttrService + */ + SecClassGenericPassword SecClass = 1 + SecClassInternetPassword SecClass = 2 +) + +// SecClassKey is the key type for SecClass +var SecClassKey = attrKey(C.CFTypeRef(C.kSecClass)) +var secClassTypeRef = map[SecClass]C.CFTypeRef{ + SecClassGenericPassword: C.CFTypeRef(C.kSecClassGenericPassword), + SecClassInternetPassword: C.CFTypeRef(C.kSecClassInternetPassword), +} + +var ( + // ServiceKey is for kSecAttrService + ServiceKey = attrKey(C.CFTypeRef(C.kSecAttrService)) + // LabelKey is for kSecAttrLabel + LabelKey = attrKey(C.CFTypeRef(C.kSecAttrLabel)) + // AccountKey is for kSecAttrAccount + AccountKey = attrKey(C.CFTypeRef(C.kSecAttrAccount)) + // AccessGroupKey is for kSecAttrAccessGroup + AccessGroupKey = attrKey(C.CFTypeRef(C.kSecAttrAccessGroup)) + // DataKey is for kSecValueData + DataKey = attrKey(C.CFTypeRef(C.kSecValueData)) + // DescriptionKey is for kSecAttrDescription + DescriptionKey = attrKey(C.CFTypeRef(C.kSecAttrDescription)) + // CreationDateKey is for kSecAttrCreationDate + CreationDateKey = attrKey(C.CFTypeRef(C.kSecAttrCreationDate)) + // ModificationDateKey is for kSecAttrModificationDate + ModificationDateKey = attrKey(C.CFTypeRef(C.kSecAttrModificationDate)) +) + +// Synchronizable is the items synchronizable status +type Synchronizable int + +const ( + // SynchronizableDefault is the default setting + SynchronizableDefault Synchronizable = 0 + // SynchronizableAny is for kSecAttrSynchronizableAny + SynchronizableAny = 1 + // SynchronizableYes enables synchronization + SynchronizableYes = 2 + // SynchronizableNo disables synchronization + SynchronizableNo = 3 +) + +// SynchronizableKey is the key type for Synchronizable +var SynchronizableKey = attrKey(C.CFTypeRef(C.kSecAttrSynchronizable)) +var syncTypeRef = map[Synchronizable]C.CFTypeRef{ + SynchronizableAny: C.CFTypeRef(C.kSecAttrSynchronizableAny), + SynchronizableYes: C.CFTypeRef(C.kCFBooleanTrue), + SynchronizableNo: C.CFTypeRef(C.kCFBooleanFalse), +} + +// Accessible is the items accessibility +type Accessible int + +const ( + // AccessibleDefault is the default + AccessibleDefault Accessible = 0 + // AccessibleWhenUnlocked is when unlocked + AccessibleWhenUnlocked = 1 + // AccessibleAfterFirstUnlock is after first unlock + AccessibleAfterFirstUnlock = 2 + // AccessibleAlways is always + AccessibleAlways = 3 + // AccessibleWhenPasscodeSetThisDeviceOnly is when passcode is set + AccessibleWhenPasscodeSetThisDeviceOnly = 4 + // AccessibleWhenUnlockedThisDeviceOnly is when unlocked for this device only + AccessibleWhenUnlockedThisDeviceOnly = 5 + // AccessibleAfterFirstUnlockThisDeviceOnly is after first unlock for this device only + AccessibleAfterFirstUnlockThisDeviceOnly = 6 + // AccessibleAccessibleAlwaysThisDeviceOnly is always for this device only + AccessibleAccessibleAlwaysThisDeviceOnly = 7 +) + +// MatchLimit is whether to limit results on query +type MatchLimit int + +const ( + // MatchLimitDefault is the default + MatchLimitDefault MatchLimit = 0 + // MatchLimitOne limits to one result + MatchLimitOne = 1 + // MatchLimitAll is no limit + MatchLimitAll = 2 +) + +// MatchLimitKey is key type for MatchLimit +var MatchLimitKey = attrKey(C.CFTypeRef(C.kSecMatchLimit)) +var matchTypeRef = map[MatchLimit]C.CFTypeRef{ + MatchLimitOne: C.CFTypeRef(C.kSecMatchLimitOne), + MatchLimitAll: C.CFTypeRef(C.kSecMatchLimitAll), +} + +// ReturnAttributesKey is key type for kSecReturnAttributes +var ReturnAttributesKey = attrKey(C.CFTypeRef(C.kSecReturnAttributes)) + +// ReturnDataKey is key type for kSecReturnData +var ReturnDataKey = attrKey(C.CFTypeRef(C.kSecReturnData)) + +// ReturnRefKey is key type for kSecReturnRef +var ReturnRefKey = attrKey(C.CFTypeRef(C.kSecReturnRef)) + +// Item for adding, querying or deleting. +type Item struct { + // Values can be string, []byte, Convertable or CFTypeRef (constant). + attr map[string]interface{} +} + +// SetSecClass sets the security class +func (k *Item) SetSecClass(sc SecClass) { + k.attr[SecClassKey] = secClassTypeRef[sc] +} + +// SetString sets a string attibute for a string key +func (k *Item) SetString(key string, s string) { + if s != "" { + k.attr[key] = s + } else { + delete(k.attr, key) + } +} + +// SetService sets the service attribute +func (k *Item) SetService(s string) { + k.SetString(ServiceKey, s) +} + +// SetAccount sets the account attribute +func (k *Item) SetAccount(a string) { + k.SetString(AccountKey, a) +} + +// SetLabel sets the label attribute +func (k *Item) SetLabel(l string) { + k.SetString(LabelKey, l) +} + +// SetDescription sets the description attribute +func (k *Item) SetDescription(s string) { + k.SetString(DescriptionKey, s) +} + +// SetData sets the data attribute +func (k *Item) SetData(b []byte) { + if b != nil { + k.attr[DataKey] = b + } else { + delete(k.attr, DataKey) + } +} + +// SetAccessGroup sets the access group attribute +func (k *Item) SetAccessGroup(ag string) { + k.SetString(AccessGroupKey, ag) +} + +// SetSynchronizable sets the synchronizable attribute +func (k *Item) SetSynchronizable(sync Synchronizable) { + if sync != SynchronizableDefault { + k.attr[SynchronizableKey] = syncTypeRef[sync] + } else { + delete(k.attr, SynchronizableKey) + } +} + +// SetAccessible sets the accessible attribute +func (k *Item) SetAccessible(accessible Accessible) { + if accessible != AccessibleDefault { + k.attr[AccessibleKey] = accessibleTypeRef[accessible] + } else { + delete(k.attr, AccessibleKey) + } +} + +// SetMatchLimit sets the match limit +func (k *Item) SetMatchLimit(matchLimit MatchLimit) { + if matchLimit != MatchLimitDefault { + k.attr[MatchLimitKey] = matchTypeRef[matchLimit] + } else { + delete(k.attr, MatchLimitKey) + } +} + +// SetReturnAttributes sets the return value type on query +func (k *Item) SetReturnAttributes(b bool) { + k.attr[ReturnAttributesKey] = b +} + +// SetReturnData enables returning data on query +func (k *Item) SetReturnData(b bool) { + k.attr[ReturnDataKey] = b +} + +// SetReturnRef enables returning references on query +func (k *Item) SetReturnRef(b bool) { + k.attr[ReturnRefKey] = b +} + +// NewItem is a new empty keychain item +func NewItem() Item { + return Item{make(map[string]interface{})} +} + +// NewGenericPassword creates a generic password item with the default keychain. This is a convenience method. +func NewGenericPassword(service string, account string, label string, data []byte, accessGroup string) Item { + item := NewItem() + item.SetSecClass(SecClassGenericPassword) + item.SetService(service) + item.SetAccount(account) + item.SetLabel(label) + item.SetData(data) + item.SetAccessGroup(accessGroup) + return item +} + +// AddItem adds a Item to a Keychain +func AddItem(item Item) error { + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDict)) + + errCode := C.SecItemAdd(cfDict, nil) + err = checkError(errCode) + return err +} + +// UpdateItem updates the queryItem with the parameters from updateItem +func UpdateItem(queryItem Item, updateItem Item) error { + cfDict, err := ConvertMapToCFDictionary(queryItem.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDict)) + cfDictUpdate, err := ConvertMapToCFDictionary(updateItem.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDictUpdate)) + errCode := C.SecItemUpdate(cfDict, cfDictUpdate) + err = checkError(errCode) + return err +} + +// QueryResult stores all possible results from queries. +// Not all fields are applicable all the time. Results depend on query. +type QueryResult struct { + Service string + Account string + AccessGroup string + Label string + Description string + Data []byte + CreationDate time.Time + ModificationDate time.Time +} + +// QueryItemRef returns query result as CFTypeRef. You must release it when you are done. +func QueryItemRef(item Item) (C.CFTypeRef, error) { + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + return 0, err + } + defer Release(C.CFTypeRef(cfDict)) + + var resultsRef C.CFTypeRef + errCode := C.SecItemCopyMatching(cfDict, &resultsRef) //nolint + if Error(errCode) == ErrorItemNotFound { + return 0, nil + } + err = checkError(errCode) + if err != nil { + return 0, err + } + return resultsRef, nil +} + +// QueryItem returns a list of query results. +func QueryItem(item Item) ([]QueryResult, error) { + resultsRef, err := QueryItemRef(item) + if err != nil { + return nil, err + } + if resultsRef == 0 { + return nil, nil + } + defer Release(resultsRef) + + results := make([]QueryResult, 0, 1) + + typeID := C.CFGetTypeID(resultsRef) + if typeID == C.CFArrayGetTypeID() { + arr := CFArrayToArray(C.CFArrayRef(resultsRef)) + for _, ref := range arr { + elementTypeID := C.CFGetTypeID(ref) + if elementTypeID == C.CFDictionaryGetTypeID() { + item, err := convertResult(C.CFDictionaryRef(ref)) + if err != nil { + return nil, err + } + results = append(results, *item) + } else { + return nil, fmt.Errorf("invalid result type (If you SetReturnRef(true) you should use QueryItemRef directly)") + } + } + } else if typeID == C.CFDictionaryGetTypeID() { + item, err := convertResult(C.CFDictionaryRef(resultsRef)) + if err != nil { + return nil, err + } + results = append(results, *item) + } else if typeID == C.CFDataGetTypeID() { + b, err := CFDataToBytes(C.CFDataRef(resultsRef)) + if err != nil { + return nil, err + } + item := QueryResult{Data: b} + results = append(results, item) + } else { + return nil, fmt.Errorf("Invalid result type: %s", CFTypeDescription(resultsRef)) + } + + return results, nil +} + +func attrKey(ref C.CFTypeRef) string { + return CFStringToString(C.CFStringRef(ref)) +} + +func convertResult(d C.CFDictionaryRef) (*QueryResult, error) { + m := CFDictionaryToMap(d) + result := QueryResult{} + for k, v := range m { + switch attrKey(k) { + case ServiceKey: + result.Service = CFStringToString(C.CFStringRef(v)) + case AccountKey: + result.Account = CFStringToString(C.CFStringRef(v)) + case AccessGroupKey: + result.AccessGroup = CFStringToString(C.CFStringRef(v)) + case LabelKey: + result.Label = CFStringToString(C.CFStringRef(v)) + case DescriptionKey: + result.Description = CFStringToString(C.CFStringRef(v)) + case DataKey: + b, err := CFDataToBytes(C.CFDataRef(v)) + if err != nil { + return nil, err + } + result.Data = b + case CreationDateKey: + result.CreationDate = CFDateToTime(C.CFDateRef(v)) + case ModificationDateKey: + result.ModificationDate = CFDateToTime(C.CFDateRef(v)) + // default: + // fmt.Printf("Unhandled key in conversion: %v = %v\n", cfTypeValue(k), cfTypeValue(v)) + } + } + return &result, nil +} + +// DeleteGenericPasswordItem removes a generic password item. +func DeleteGenericPasswordItem(service string, account string) error { + item := NewItem() + item.SetSecClass(SecClassGenericPassword) + item.SetService(service) + item.SetAccount(account) + return DeleteItem(item) +} + +// DeleteItem removes a Item +func DeleteItem(item Item) error { + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDict)) + + errCode := C.SecItemDelete(cfDict) + return checkError(errCode) +} + +// GetAccountsForService is deprecated +func GetAccountsForService(service string) ([]string, error) { + return GetGenericPasswordAccounts(service) +} + +// GetGenericPasswordAccounts returns generic password accounts for service. This is a convenience method. +func GetGenericPasswordAccounts(service string) ([]string, error) { + query := NewItem() + query.SetSecClass(SecClassGenericPassword) + query.SetService(service) + query.SetMatchLimit(MatchLimitAll) + query.SetReturnAttributes(true) + results, err := QueryItem(query) + if err != nil { + return nil, err + } + + accounts := make([]string, 0, len(results)) + for _, r := range results { + accounts = append(accounts, r.Account) + } + + return accounts, nil +} + +// GetGenericPassword returns password data for service and account. This is a convenience method. +// If item is not found returns nil, nil. +func GetGenericPassword(service string, account string, label string, accessGroup string) ([]byte, error) { + query := NewItem() + query.SetSecClass(SecClassGenericPassword) + query.SetService(service) + query.SetAccount(account) + query.SetLabel(label) + query.SetAccessGroup(accessGroup) + query.SetMatchLimit(MatchLimitOne) + query.SetReturnData(true) + results, err := QueryItem(query) + if err != nil { + return nil, err + } + if len(results) > 1 { + return nil, fmt.Errorf("Too many results") + } + if len(results) == 1 { + return results[0].Data, nil + } + return nil, nil +} diff --git a/vendor/github.com/99designs/go-keychain/macos.go b/vendor/github.com/99designs/go-keychain/macos.go new file mode 100644 index 0000000000..4004349af8 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/macos.go @@ -0,0 +1,272 @@ +// +build darwin,!ios + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation -framework Security +#cgo CFLAGS: -w + +#include +#include +*/ +import "C" +import ( + "os" + "unsafe" +) + +// AccessibleKey is key for kSecAttrAccessible +var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible)) +var accessibleTypeRef = map[Accessible]C.CFTypeRef{ + AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked), + AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock), + AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways), + AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly), + AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), + AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly), + + // Only available in 10.10 + //AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), +} + +var ( + // AccessKey is key for kSecAttrAccess + AccessKey = attrKey(C.CFTypeRef(C.kSecAttrAccess)) +) + +// createAccess creates a SecAccessRef as CFTypeRef. +// The returned SecAccessRef, if non-nil, must be released via CFRelease. +func createAccess(label string, trustedApplications []string) (C.CFTypeRef, error) { + var err error + var labelRef C.CFStringRef + if labelRef, err = StringToCFString(label); err != nil { + return 0, err + } + defer C.CFRelease(C.CFTypeRef(labelRef)) + + var trustedApplicationsArray C.CFArrayRef + if trustedApplications != nil { + if len(trustedApplications) > 0 { + // Always prepend with empty string which signifies that we + // include a NULL application, which means ourselves. + trustedApplications = append([]string{""}, trustedApplications...) + } + + var trustedApplicationsRefs []C.CFTypeRef + for _, trustedApplication := range trustedApplications { + trustedApplicationRef, createErr := createTrustedApplication(trustedApplication) + if createErr != nil { + return 0, createErr + } + defer C.CFRelease(trustedApplicationRef) + trustedApplicationsRefs = append(trustedApplicationsRefs, trustedApplicationRef) + } + + trustedApplicationsArray = ArrayToCFArray(trustedApplicationsRefs) + defer C.CFRelease(C.CFTypeRef(trustedApplicationsArray)) + } + + var access C.SecAccessRef + errCode := C.SecAccessCreate(labelRef, trustedApplicationsArray, &access) //nolint + err = checkError(errCode) + if err != nil { + return 0, err + } + + return C.CFTypeRef(access), nil +} + +// createTrustedApplication creates a SecTrustedApplicationRef as a CFTypeRef. +// The returned SecTrustedApplicationRef, if non-nil, must be released via CFRelease. +func createTrustedApplication(trustedApplication string) (C.CFTypeRef, error) { + var trustedApplicationCStr *C.char + if trustedApplication != "" { + trustedApplicationCStr = C.CString(trustedApplication) + defer C.free(unsafe.Pointer(trustedApplicationCStr)) + } + + var trustedApplicationRef C.SecTrustedApplicationRef + errCode := C.SecTrustedApplicationCreateFromPath(trustedApplicationCStr, &trustedApplicationRef) //nolint + err := checkError(errCode) + if err != nil { + return 0, err + } + + return C.CFTypeRef(trustedApplicationRef), nil +} + +// Access defines whats applications can use the keychain item +type Access struct { + Label string + TrustedApplications []string +} + +// Convert converts Access to CFTypeRef. +// The returned CFTypeRef, if non-nil, must be released via CFRelease. +func (a Access) Convert() (C.CFTypeRef, error) { + return createAccess(a.Label, a.TrustedApplications) +} + +// SetAccess sets Access on Item +func (k *Item) SetAccess(a *Access) { + if a != nil { + k.attr[AccessKey] = a + } else { + delete(k.attr, AccessKey) + } +} + +// DeleteItemRef deletes a keychain item reference. +func DeleteItemRef(ref C.CFTypeRef) error { + errCode := C.SecKeychainItemDelete(C.SecKeychainItemRef(ref)) + return checkError(errCode) +} + +var ( + // KeychainKey is key for kSecUseKeychain + KeychainKey = attrKey(C.CFTypeRef(C.kSecUseKeychain)) + // MatchSearchListKey is key for kSecMatchSearchList + MatchSearchListKey = attrKey(C.CFTypeRef(C.kSecMatchSearchList)) +) + +// Keychain represents the path to a specific OSX keychain +type Keychain struct { + path string +} + +// NewKeychain creates a new keychain file with a password +func NewKeychain(path string, password string) (Keychain, error) { + return newKeychain(path, password, false) +} + +// NewKeychainWithPrompt creates a new Keychain and prompts user for password +func NewKeychainWithPrompt(path string) (Keychain, error) { + return newKeychain(path, "", true) +} + +func newKeychain(path, password string, promptUser bool) (Keychain, error) { + pathRef := C.CString(path) + defer C.free(unsafe.Pointer(pathRef)) + + var errCode C.OSStatus + var kref C.SecKeychainRef + + if promptUser { + errCode = C.SecKeychainCreate(pathRef, C.UInt32(0), nil, C.Boolean(1), 0, &kref) //nolint + } else { + passwordRef := C.CString(password) + defer C.free(unsafe.Pointer(passwordRef)) + errCode = C.SecKeychainCreate(pathRef, C.UInt32(len(password)), unsafe.Pointer(passwordRef), C.Boolean(0), 0, &kref) //nolint + } + + if err := checkError(errCode); err != nil { + return Keychain{}, err + } + + // TODO: Without passing in kref I get 'One or more parameters passed to the function were not valid (-50)' + defer Release(C.CFTypeRef(kref)) + + return Keychain{ + path: path, + }, nil +} + +// NewWithPath to use an existing keychain +func NewWithPath(path string) Keychain { + return Keychain{ + path: path, + } +} + +// Status returns the status of the keychain +func (kc Keychain) Status() error { + // returns no error even if it doesn't exist + kref, err := openKeychainRef(kc.path) + if err != nil { + return err + } + defer C.CFRelease(C.CFTypeRef(kref)) + + var status C.SecKeychainStatus + return checkError(C.SecKeychainGetStatus(kref, &status)) +} + +// The returned SecKeychainRef, if non-nil, must be released via CFRelease. +func openKeychainRef(path string) (C.SecKeychainRef, error) { + pathName := C.CString(path) + defer C.free(unsafe.Pointer(pathName)) + + var kref C.SecKeychainRef + if err := checkError(C.SecKeychainOpen(pathName, &kref)); err != nil { //nolint + return 0, err + } + + return kref, nil +} + +// UnlockAtPath unlocks keychain at path +func UnlockAtPath(path string, password string) error { + kref, err := openKeychainRef(path) + defer Release(C.CFTypeRef(kref)) + if err != nil { + return err + } + passwordRef := C.CString(password) + defer C.free(unsafe.Pointer(passwordRef)) + return checkError(C.SecKeychainUnlock(kref, C.UInt32(len(password)), unsafe.Pointer(passwordRef), C.Boolean(1))) +} + +// LockAtPath locks keychain at path +func LockAtPath(path string) error { + kref, err := openKeychainRef(path) + defer Release(C.CFTypeRef(kref)) + if err != nil { + return err + } + return checkError(C.SecKeychainLock(kref)) +} + +// Delete the Keychain +func (kc *Keychain) Delete() error { + return os.Remove(kc.path) +} + +// Convert Keychain to CFTypeRef. +// The returned CFTypeRef, if non-nil, must be released via CFRelease. +func (kc Keychain) Convert() (C.CFTypeRef, error) { + keyRef, err := openKeychainRef(kc.path) + return C.CFTypeRef(keyRef), err +} + +type keychainArray []Keychain + +// Convert the keychainArray to a CFTypeRef. +// The returned CFTypeRef, if non-nil, must be released via CFRelease. +func (ka keychainArray) Convert() (C.CFTypeRef, error) { + var refs = make([]C.CFTypeRef, len(ka)) + var err error + + for idx, kc := range ka { + if refs[idx], err = kc.Convert(); err != nil { + // If we error trying to convert lets release any we converted before + for _, ref := range refs { + if ref != 0 { + Release(ref) + } + } + return 0, err + } + } + + return C.CFTypeRef(ArrayToCFArray(refs)), nil +} + +// SetMatchSearchList sets match type on keychains +func (k *Item) SetMatchSearchList(karr ...Keychain) { + k.attr[MatchSearchListKey] = keychainArray(karr) +} + +// UseKeychain tells item to use the specified Keychain +func (k *Item) UseKeychain(kc Keychain) { + k.attr[KeychainKey] = kc +} diff --git a/vendor/github.com/99designs/go-keychain/util.go b/vendor/github.com/99designs/go-keychain/util.go new file mode 100644 index 0000000000..29cbfc6629 --- /dev/null +++ b/vendor/github.com/99designs/go-keychain/util.go @@ -0,0 +1,31 @@ +package keychain + +import ( + "crypto/rand" + "encoding/base32" + "strings" +) + +var randRead = rand.Read + +// RandomID returns random ID (base32) string with prefix, using 256 bits as +// recommended by tptacek: https://gist.github.com/tqbf/be58d2d39690c3b366ad +func RandomID(prefix string) (string, error) { + buf, err := RandBytes(32) + if err != nil { + return "", err + } + str := base32.StdEncoding.EncodeToString(buf) + str = strings.Replace(str, "=", "", -1) + str = prefix + str + return str, nil +} + +// RandBytes returns random bytes of length +func RandBytes(length int) ([]byte, error) { + buf := make([]byte, length) + if _, err := randRead(buf); err != nil { + return nil, err + } + return buf, nil +} diff --git a/vendor/github.com/99designs/keyring/.gitattributes b/vendor/github.com/99designs/keyring/.gitattributes new file mode 100644 index 0000000000..d207b1802b --- /dev/null +++ b/vendor/github.com/99designs/keyring/.gitattributes @@ -0,0 +1 @@ +*.go text eol=lf diff --git a/vendor/github.com/99designs/keyring/.gitignore b/vendor/github.com/99designs/keyring/.gitignore new file mode 100644 index 0000000000..8000dd9db4 --- /dev/null +++ b/vendor/github.com/99designs/keyring/.gitignore @@ -0,0 +1 @@ +.vagrant diff --git a/vendor/github.com/99designs/keyring/.golangci.yml b/vendor/github.com/99designs/keyring/.golangci.yml new file mode 100644 index 0000000000..f83428efa7 --- /dev/null +++ b/vendor/github.com/99designs/keyring/.golangci.yml @@ -0,0 +1,29 @@ +linters: + enable: + - bodyclose + - contextcheck + - depguard + - durationcheck + - dupl + - errchkjson + - errname + - exhaustive + - exportloopref + - gocritic + - gofmt + - goimports + - makezero + - misspell + - nakedret + - nilerr + - nilnil + - noctx + - prealloc + - revive + # - rowserrcheck + - thelper + - tparallel + - unconvert + - unparam + # - wastedassign + - whitespace diff --git a/vendor/github.com/99designs/keyring/LICENSE b/vendor/github.com/99designs/keyring/LICENSE new file mode 100644 index 0000000000..0fe9e46922 --- /dev/null +++ b/vendor/github.com/99designs/keyring/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 99designs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/99designs/keyring/README.md b/vendor/github.com/99designs/keyring/README.md new file mode 100644 index 0000000000..629a6aaf89 --- /dev/null +++ b/vendor/github.com/99designs/keyring/README.md @@ -0,0 +1,67 @@ +Keyring +======= +[![Build Status](https://github.com/99designs/keyring/workflows/Continuous%20Integration/badge.svg)](https://github.com/99designs/keyring/actions) +[![Documentation](https://godoc.org/github.com/99designs/keyring?status.svg)](https://godoc.org/github.com/99designs/keyring) + +Keyring provides a common interface to a range of secure credential storage services. Originally developed as part of [AWS Vault](https://github.com/99designs/aws-vault), a command line tool for securely managing AWS access from developer workstations. + +Currently Keyring supports the following backends + * [macOS Keychain](https://support.apple.com/en-au/guide/keychain-access/welcome/mac) + * [Windows Credential Manager](https://support.microsoft.com/en-au/help/4026814/windows-accessing-credential-manager) + * Secret Service ([Gnome Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5)) + * [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5) + * [Pass](https://www.passwordstore.org/) + * [Encrypted file (JWT)](https://datatracker.ietf.org/doc/html/rfc7519) + * [KeyCtl](https://linux.die.net/man/1/keyctl) + + +## Usage + +The short version of how to use keyring is shown below. + +```go +ring, _ := keyring.Open(keyring.Config{ + ServiceName: "example", +}) + +_ = ring.Set(keyring.Item{ + Key: "foo", + Data: []byte("secret-bar"), +}) + +i, _ := ring.Get("foo") + +fmt.Printf("%s", i.Data) +``` + +For more detail on the API please check [the keyring godocs](https://godoc.org/github.com/99designs/keyring) + + +## Testing + +[Vagrant](https://www.vagrantup.com/) is used to create linux and windows test environments. + +```bash +# Start vagrant +vagrant up + +# Run go tests on all platforms +./bin/go-test +``` + + +## Contributing + +Contributions to the keyring package are most welcome from engineers of all backgrounds and skill levels. In particular the addition of extra backends across popular operating systems would be appreciated. + +This project will adhere to the [Go Community Code of Conduct](https://golang.org/conduct) in the github provided discussion spaces, with the moderators being the 99designs engineering team. + +To make a contribution: + + * Fork the repository + * Make your changes on the fork + * Submit a pull request back to this repo with a clear description of the problem you're solving + * Ensure your PR passes all current (and new) tests + * Ideally verify that [aws-vault](https://github.com/99designs/aws-vault) works with your changes (optional) + +...and we'll do our best to get your work merged in diff --git a/vendor/github.com/99designs/keyring/Vagrantfile b/vendor/github.com/99designs/keyring/Vagrantfile new file mode 100644 index 0000000000..7d30d2ac42 --- /dev/null +++ b/vendor/github.com/99designs/keyring/Vagrantfile @@ -0,0 +1,85 @@ +Vagrant.configure("2") do |config| + + config.vm.define "linux" do |linux| + linux.vm.box = "generic/fedora32" + + linux.vm.provider "virtualbox" do |vb| + vb.gui = true + vb.memory = 2048 + vb.cpus = 2 + + # VBoxVGA flickers constantly, use vmsvga instead which doesn't have that problem + vb.customize ["modifyvm", :id, "--graphicscontroller", "vmsvga"] + end + + # mount the project into /keyring + linux.vm.synced_folder ".", "/keyring" + + # install gnome desktop and auto login + linux.vm.provision "shell", inline: "sudo dnf install -y --exclude='gnome-initial-setup' @gnome-desktop langpacks-en" + linux.vm.provision "shell", inline: <<-SHELL + sudo sed -i -e 's/\\[daemon\\]/\\[daemon\\]\\nAutomaticLoginEnable=True\\nAutomaticLogin=vagrant\\n/' \ + /etc/gdm/custom.conf + SHELL + linux.vm.provision "shell", inline: "sudo systemctl set-default graphical.target" + linux.vm.provision "shell", inline: "sudo systemctl isolate graphical.target" + + # set the root password - sometimes prompts show up in gnome needing to install software + linux.vm.provision "shell", inline: "echo 'vagrant' | sudo passwd root --stdin" + + # install gnome keyring + linux.vm.provision "shell", inline: "sudo dnf install -y gnome-keyring seahorse" + + # install kwallet + linux.vm.provision "shell", inline: "sudo dnf install -y kwalletmanager5" + + # install pass + linux.vm.provision "shell", inline: "sudo dnf install -y pass" + + # install golang + linux.vm.provision "shell", inline: "sudo dnf install -y go" + end + + + config.vm.define "windows" do |windows| + windows.vm.box = "StefanScherer/windows_10" + + windows.vm.provider "virtualbox" do |vb| + vb.gui = true + vb.memory = 2048 + vb.cpus = 2 + end + + # mount the project into c:\keyring + windows.vm.synced_folder ".", "/keyring" + + # install chocolately + windows.vm.provision "shell", privileged: true, inline: <<-SHELL + Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) + choco feature disable -n=showDownloadProgress + SHELL + + # install golang + windows.vm.provision "shell", privileged: true, inline: "choco install -y git golang" + end + + config.vm.post_up_message = <<-MESSAGE + There are 2 vagrant boxes: + - linux + - OS: Fedora 32 with Gnome Desktop + - The keyring directory is mounted at /keyring + - Get a shell with 'vagrant ssh linux' + - When running go test, you'll need to use the GUI to click "Continue" on the prompts + - After provisioning, adjusting the virtualbox GUI window size doesn't cause the resolution to update. A 'vagrant reload linux' solves the problem + - windows + - OS: Windows 10 + - The keyring directory is mounted at C:\keyring + - Get a shell by starting PowerShell in the GUI + - You can run commands remotely using 'vagrant winrm -e windows CMD'. You'll need the -e (elevated privileges) if you want to interact with wincred + + Automated scripts for running go test on vagrant boxes (run these locally): + - ./bin/go-test-linux - Run tests on Linux + - ./bin/go-test-windows - Run tests on Windows + - ./bin/go-test - Run all tests - locally, linux and windows + MESSAGE +end diff --git a/vendor/github.com/99designs/keyring/array.go b/vendor/github.com/99designs/keyring/array.go new file mode 100644 index 0000000000..3179cb592b --- /dev/null +++ b/vendor/github.com/99designs/keyring/array.go @@ -0,0 +1,54 @@ +package keyring + +// ArrayKeyring is a mock/non-secure backend that meets the Keyring interface. +// It is intended to be used to aid unit testing of code that relies on the package. +// NOTE: Do not use in production code. +type ArrayKeyring struct { + items map[string]Item +} + +// NewArrayKeyring returns an ArrayKeyring, optionally constructed with an initial slice +// of items. +func NewArrayKeyring(initial []Item) *ArrayKeyring { + kr := &ArrayKeyring{} + for _, i := range initial { + _ = kr.Set(i) + } + return kr +} + +// Get returns an Item matching Key. +func (k *ArrayKeyring) Get(key string) (Item, error) { + if i, ok := k.items[key]; ok { + return i, nil + } + return Item{}, ErrKeyNotFound +} + +// Set will store an item on the mock Keyring. +func (k *ArrayKeyring) Set(i Item) error { + if k.items == nil { + k.items = map[string]Item{} + } + k.items[i.Key] = i + return nil +} + +// Remove will delete an Item from the Keyring. +func (k *ArrayKeyring) Remove(key string) error { + delete(k.items, key) + return nil +} + +// Keys provides a slice of all Item keys on the Keyring. +func (k *ArrayKeyring) Keys() ([]string, error) { + var keys = []string{} + for key := range k.items { + keys = append(keys, key) + } + return keys, nil +} + +func (k *ArrayKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNeedsCredentials +} diff --git a/vendor/github.com/99designs/keyring/config.go b/vendor/github.com/99designs/keyring/config.go new file mode 100644 index 0000000000..590af7cf70 --- /dev/null +++ b/vendor/github.com/99designs/keyring/config.go @@ -0,0 +1,58 @@ +package keyring + +// Config contains configuration for keyring. +type Config struct { + // AllowedBackends is a whitelist of backend providers that can be used. Nil means all available. + AllowedBackends []BackendType + + // ServiceName is a generic service name that is used by backends that support the concept + ServiceName string + + // MacOSKeychainNameKeychainName is the name of the macOS keychain that is used + KeychainName string + + // KeychainTrustApplication is whether the calling application should be trusted by default by items + KeychainTrustApplication bool + + // KeychainSynchronizable is whether the item can be synchronized to iCloud + KeychainSynchronizable bool + + // KeychainAccessibleWhenUnlocked is whether the item is accessible when the device is locked + KeychainAccessibleWhenUnlocked bool + + // KeychainPasswordFunc is an optional function used to prompt the user for a password + KeychainPasswordFunc PromptFunc + + // FilePasswordFunc is a required function used to prompt the user for a password + FilePasswordFunc PromptFunc + + // FileDir is the directory that keyring files are stored in, ~/ is resolved to the users' home dir + FileDir string + + // KeyCtlScope is the scope of the kernel keyring (either "user", "session", "process" or "thread") + KeyCtlScope string + + // KeyCtlPerm is the permission mask to use for new keys + KeyCtlPerm uint32 + + // KWalletAppID is the application id for KWallet + KWalletAppID string + + // KWalletFolder is the folder for KWallet + KWalletFolder string + + // LibSecretCollectionName is the name collection in secret-service + LibSecretCollectionName string + + // PassDir is the pass password-store directory, ~/ is resolved to the users' home dir + PassDir string + + // PassCmd is the name of the pass executable + PassCmd string + + // PassPrefix is a string prefix to prepend to the item path stored in pass + PassPrefix string + + // WinCredPrefix is a string prefix to prepend to the key name + WinCredPrefix string +} diff --git a/vendor/github.com/99designs/keyring/docker-compose.yml b/vendor/github.com/99designs/keyring/docker-compose.yml new file mode 100644 index 0000000000..90202209e5 --- /dev/null +++ b/vendor/github.com/99designs/keyring/docker-compose.yml @@ -0,0 +1,7 @@ +version: "3.9" +services: + keyring: + image: golang:1.19 + volumes: + - .:/usr/local/src/keyring + working_dir: /usr/local/src/keyring diff --git a/vendor/github.com/99designs/keyring/file.go b/vendor/github.com/99designs/keyring/file.go new file mode 100644 index 0000000000..0d25b57275 --- /dev/null +++ b/vendor/github.com/99designs/keyring/file.go @@ -0,0 +1,180 @@ +package keyring + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "time" + + jose "github.com/dvsekhvalnov/jose2go" + "github.com/mtibben/percent" +) + +func init() { + supportedBackends[FileBackend] = opener(func(cfg Config) (Keyring, error) { + return &fileKeyring{ + dir: cfg.FileDir, + passwordFunc: cfg.FilePasswordFunc, + }, nil + }) +} + +var filenameEscape = func(s string) string { + return percent.Encode(s, "/") +} +var filenameUnescape = percent.Decode + +type fileKeyring struct { + dir string + passwordFunc PromptFunc + password string +} + +func (k *fileKeyring) resolveDir() (string, error) { + if k.dir == "" { + return "", fmt.Errorf("No directory provided for file keyring") + } + + dir, err := ExpandTilde(k.dir) + if err != nil { + return "", err + } + + stat, err := os.Stat(dir) + if os.IsNotExist(err) { + err = os.MkdirAll(dir, 0700) + } else if err != nil && stat != nil && !stat.IsDir() { + err = fmt.Errorf("%s is a file, not a directory", dir) + } + + return dir, err +} + +func (k *fileKeyring) unlock() error { + dir, err := k.resolveDir() + if err != nil { + return err + } + + if k.password == "" { + pwd, err := k.passwordFunc(fmt.Sprintf("Enter passphrase to unlock %q", dir)) + if err != nil { + return err + } + k.password = pwd + } + + return nil +} + +func (k *fileKeyring) Get(key string) (Item, error) { + filename, err := k.filename(key) + if err != nil { + return Item{}, err + } + + bytes, err := os.ReadFile(filename) + if os.IsNotExist(err) { + return Item{}, ErrKeyNotFound + } else if err != nil { + return Item{}, err + } + + if err = k.unlock(); err != nil { + return Item{}, err + } + + payload, _, err := jose.Decode(string(bytes), k.password) + if err != nil { + return Item{}, err + } + + var decoded Item + err = json.Unmarshal([]byte(payload), &decoded) + + return decoded, err +} + +func (k *fileKeyring) GetMetadata(key string) (Metadata, error) { + filename, err := k.filename(key) + if err != nil { + return Metadata{}, err + } + + stat, err := os.Stat(filename) + if os.IsNotExist(err) { + return Metadata{}, ErrKeyNotFound + } else if err != nil { + return Metadata{}, err + } + + // For the File provider, all internal data is encrypted, not just the + // credentials. Thus we only have the timestamps. Return a nil *Item. + // + // If we want to change this ... how portable are extended file attributes + // these days? Would it break user expectations of the security model to + // leak data into those? I'm hesitant to do so. + + return Metadata{ + ModificationTime: stat.ModTime(), + }, nil +} + +func (k *fileKeyring) Set(i Item) error { + bytes, err := json.Marshal(i) + if err != nil { + return err + } + + if err = k.unlock(); err != nil { + return err + } + + token, err := jose.Encrypt(string(bytes), jose.PBES2_HS256_A128KW, jose.A256GCM, k.password, + jose.Headers(map[string]interface{}{ + "created": time.Now().String(), + })) + if err != nil { + return err + } + + filename, err := k.filename(i.Key) + if err != nil { + return err + } + return os.WriteFile(filename, []byte(token), 0600) +} + +func (k *fileKeyring) filename(key string) (string, error) { + dir, err := k.resolveDir() + if err != nil { + return "", err + } + + return filepath.Join(dir, filenameEscape(key)), nil +} + +func (k *fileKeyring) Remove(key string) error { + filename, err := k.filename(key) + if err != nil { + return err + } + + return os.Remove(filename) +} + +func (k *fileKeyring) Keys() ([]string, error) { + dir, err := k.resolveDir() + if err != nil { + return nil, err + } + + var keys = []string{} + files, _ := os.ReadDir(dir) + for _, f := range files { + keys = append(keys, filenameUnescape(f.Name())) + } + + return keys, nil +} diff --git a/vendor/github.com/99designs/keyring/keychain.go b/vendor/github.com/99designs/keyring/keychain.go new file mode 100644 index 0000000000..d4e634e1f5 --- /dev/null +++ b/vendor/github.com/99designs/keyring/keychain.go @@ -0,0 +1,301 @@ +//go:build darwin && cgo +// +build darwin,cgo + +package keyring + +import ( + "errors" + "fmt" + + gokeychain "github.com/99designs/go-keychain" +) + +type keychain struct { + path string + service string + + passwordFunc PromptFunc + + isSynchronizable bool + isAccessibleWhenUnlocked bool + isTrusted bool +} + +func init() { + supportedBackends[KeychainBackend] = opener(func(cfg Config) (Keyring, error) { + kc := &keychain{ + service: cfg.ServiceName, + passwordFunc: cfg.KeychainPasswordFunc, + + // Set the isAccessibleWhenUnlocked to the boolean value of + // KeychainAccessibleWhenUnlocked is a shorthand for setting the accessibility value. + // See: https://developer.apple.com/documentation/security/ksecattraccessiblewhenunlocked + isAccessibleWhenUnlocked: cfg.KeychainAccessibleWhenUnlocked, + } + if cfg.KeychainName != "" { + kc.path = cfg.KeychainName + ".keychain" + } + if cfg.KeychainTrustApplication { + kc.isTrusted = true + } + return kc, nil + }) +} + +func (k *keychain) Get(key string) (Item, error) { + query := gokeychain.NewItem() + query.SetSecClass(gokeychain.SecClassGenericPassword) + query.SetService(k.service) + query.SetAccount(key) + query.SetMatchLimit(gokeychain.MatchLimitOne) + query.SetReturnAttributes(true) + query.SetReturnData(true) + + if k.path != "" { + // When we are querying, we don't create by default + query.SetMatchSearchList(gokeychain.NewWithPath(k.path)) + } + + debugf("Querying keychain for service=%q, account=%q, keychain=%q", k.service, key, k.path) + results, err := gokeychain.QueryItem(query) + if err == gokeychain.ErrorItemNotFound || len(results) == 0 { + debugf("No results found") + return Item{}, ErrKeyNotFound + } + + if err != nil { + debugf("Error: %#v", err) + return Item{}, err + } + + item := Item{ + Key: key, + Data: results[0].Data, + Label: results[0].Label, + Description: results[0].Description, + } + + debugf("Found item %q", results[0].Label) + return item, nil +} + +func (k *keychain) GetMetadata(key string) (Metadata, error) { + query := gokeychain.NewItem() + query.SetSecClass(gokeychain.SecClassGenericPassword) + query.SetService(k.service) + query.SetAccount(key) + query.SetMatchLimit(gokeychain.MatchLimitOne) + query.SetReturnAttributes(true) + query.SetReturnData(false) + query.SetReturnRef(true) + + debugf("Querying keychain for metadata of service=%q, account=%q, keychain=%q", k.service, key, k.path) + results, err := gokeychain.QueryItem(query) + if err == gokeychain.ErrorItemNotFound || len(results) == 0 { + debugf("No results found") + return Metadata{}, ErrKeyNotFound + } else if err != nil { + debugf("Error: %#v", err) + return Metadata{}, err + } + + md := Metadata{ + Item: &Item{ + Key: key, + Label: results[0].Label, + Description: results[0].Description, + }, + ModificationTime: results[0].ModificationDate, + } + + debugf("Found metadata for %q", md.Item.Label) + + return md, nil +} + +func (k *keychain) updateItem(kc gokeychain.Keychain, kcItem gokeychain.Item, account string) error { + queryItem := gokeychain.NewItem() + queryItem.SetSecClass(gokeychain.SecClassGenericPassword) + queryItem.SetService(k.service) + queryItem.SetAccount(account) + queryItem.SetMatchLimit(gokeychain.MatchLimitOne) + queryItem.SetReturnAttributes(true) + + if k.path != "" { + queryItem.SetMatchSearchList(kc) + } + + results, err := gokeychain.QueryItem(queryItem) + if err != nil { + return fmt.Errorf("Failed to query keychain: %v", err) + } + if len(results) == 0 { + return errors.New("no results") + } + + // Don't call SetAccess() as this will cause multiple prompts on update, even when we are not updating the AccessList + kcItem.SetAccess(nil) + + if err := gokeychain.UpdateItem(queryItem, kcItem); err != nil { + return fmt.Errorf("Failed to update item in keychain: %v", err) + } + + return nil +} + +func (k *keychain) Set(item Item) error { + var kc gokeychain.Keychain + + // when we are setting a value, we create or open + if k.path != "" { + var err error + kc, err = k.createOrOpen() + if err != nil { + return err + } + } + + kcItem := gokeychain.NewItem() + kcItem.SetSecClass(gokeychain.SecClassGenericPassword) + kcItem.SetService(k.service) + kcItem.SetAccount(item.Key) + kcItem.SetLabel(item.Label) + kcItem.SetDescription(item.Description) + kcItem.SetData(item.Data) + + if k.path != "" { + kcItem.UseKeychain(kc) + } + + if k.isSynchronizable && !item.KeychainNotSynchronizable { + kcItem.SetSynchronizable(gokeychain.SynchronizableYes) + } + + if k.isAccessibleWhenUnlocked { + kcItem.SetAccessible(gokeychain.AccessibleWhenUnlocked) + } + + isTrusted := k.isTrusted && !item.KeychainNotTrustApplication + + if isTrusted { + debugf("Keychain item trusts keyring") + kcItem.SetAccess(&gokeychain.Access{ + Label: item.Label, + TrustedApplications: nil, + }) + } else { + debugf("Keychain item doesn't trust keyring") + kcItem.SetAccess(&gokeychain.Access{ + Label: item.Label, + TrustedApplications: []string{}, + }) + } + + debugf("Adding service=%q, label=%q, account=%q, trusted=%v to osx keychain %q", k.service, item.Label, item.Key, isTrusted, k.path) + + err := gokeychain.AddItem(kcItem) + + if err == gokeychain.ErrorDuplicateItem { + debugf("Item already exists, updating") + err = k.updateItem(kc, kcItem, item.Key) + } + + if err != nil { + return err + } + + return nil +} + +func (k *keychain) Remove(key string) error { + item := gokeychain.NewItem() + item.SetSecClass(gokeychain.SecClassGenericPassword) + item.SetService(k.service) + item.SetAccount(key) + + if k.path != "" { + kc := gokeychain.NewWithPath(k.path) + + if err := kc.Status(); err != nil { + if err == gokeychain.ErrorNoSuchKeychain { + return ErrKeyNotFound + } + return err + } + + item.SetMatchSearchList(kc) + } + + debugf("Removing keychain item service=%q, account=%q, keychain %q", k.service, key, k.path) + err := gokeychain.DeleteItem(item) + if err == gokeychain.ErrorItemNotFound { + return ErrKeyNotFound + } + + return err +} + +func (k *keychain) Keys() ([]string, error) { + query := gokeychain.NewItem() + query.SetSecClass(gokeychain.SecClassGenericPassword) + query.SetService(k.service) + query.SetMatchLimit(gokeychain.MatchLimitAll) + query.SetReturnAttributes(true) + + if k.path != "" { + kc := gokeychain.NewWithPath(k.path) + + if err := kc.Status(); err != nil { + if err == gokeychain.ErrorNoSuchKeychain { + return []string{}, nil + } + return nil, err + } + + query.SetMatchSearchList(kc) + } + + debugf("Querying keychain for service=%q, keychain=%q", k.service, k.path) + results, err := gokeychain.QueryItem(query) + if err != nil { + return nil, err + } + + debugf("Found %d results", len(results)) + accountNames := make([]string, len(results)) + for idx, r := range results { + accountNames[idx] = r.Account + } + + return accountNames, nil +} + +func (k *keychain) createOrOpen() (gokeychain.Keychain, error) { + kc := gokeychain.NewWithPath(k.path) + + debugf("Checking keychain status") + err := kc.Status() + if err == nil { + debugf("Keychain status returned nil, keychain exists") + return kc, nil + } + + debugf("Keychain status returned error: %v", err) + + if err != gokeychain.ErrorNoSuchKeychain { + return gokeychain.Keychain{}, err + } + + if k.passwordFunc == nil { + debugf("Creating keychain %s with prompt", k.path) + return gokeychain.NewKeychainWithPrompt(k.path) + } + + passphrase, err := k.passwordFunc("Enter passphrase for keychain") + if err != nil { + return gokeychain.Keychain{}, err + } + + debugf("Creating keychain %s with provided password", k.path) + return gokeychain.NewKeychain(k.path, passphrase) +} diff --git a/vendor/github.com/99designs/keyring/keyctl.go b/vendor/github.com/99designs/keyring/keyctl.go new file mode 100644 index 0000000000..bd8018ab7f --- /dev/null +++ b/vendor/github.com/99designs/keyring/keyctl.go @@ -0,0 +1,327 @@ +//go:build linux +// +build linux + +package keyring + +import ( + "errors" + "fmt" + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +//nolint:revive +const ( + KEYCTL_PERM_VIEW = uint32(1 << 0) + KEYCTL_PERM_READ = uint32(1 << 1) + KEYCTL_PERM_WRITE = uint32(1 << 2) + KEYCTL_PERM_SEARCH = uint32(1 << 3) + KEYCTL_PERM_LINK = uint32(1 << 4) + KEYCTL_PERM_SETATTR = uint32(1 << 5) + KEYCTL_PERM_ALL = uint32((1 << 6) - 1) + + KEYCTL_PERM_OTHERS = 0 + KEYCTL_PERM_GROUP = 8 + KEYCTL_PERM_USER = 16 + KEYCTL_PERM_PROCESS = 24 +) + +// GetPermissions constructs the permission mask from the elements. +func GetPermissions(process, user, group, others uint32) uint32 { + perm := others << KEYCTL_PERM_OTHERS + perm |= group << KEYCTL_PERM_GROUP + perm |= user << KEYCTL_PERM_USER + perm |= process << KEYCTL_PERM_PROCESS + + return perm +} + +// GetKeyringIDForScope get the keyring ID for a given scope. +func GetKeyringIDForScope(scope string) (int32, error) { + ringRef, err := getKeyringForScope(scope) + if err != nil { + return 0, err + } + id, err := unix.KeyctlGetKeyringID(int(ringRef), false) + return int32(id), err +} + +type keyctlKeyring struct { + keyring int32 + perm uint32 +} + +func init() { + supportedBackends[KeyCtlBackend] = opener(func(cfg Config) (Keyring, error) { + keyring := keyctlKeyring{} + if cfg.KeyCtlPerm > 0 { + keyring.perm = cfg.KeyCtlPerm + } + + parent, err := getKeyringForScope(cfg.KeyCtlScope) + if err != nil { + return nil, fmt.Errorf("accessing %q keyring failed: %v", cfg.KeyCtlScope, err) + } + + // Check for named keyrings + keyring.keyring = parent + if cfg.ServiceName != "" { + namedKeyring, err := keyctlSearch(parent, "keyring", cfg.ServiceName) + if err != nil { + if !errors.Is(err, syscall.ENOKEY) { + return nil, fmt.Errorf("opening named %q keyring failed: %v", cfg.KeyCtlScope, err) + } + + // Keyring does not yet exist, create it + namedKeyring, err = keyring.createNamedKeyring(parent, cfg.ServiceName) + if err != nil { + return nil, fmt.Errorf("creating named %q keyring failed: %v", cfg.KeyCtlScope, err) + } + } + keyring.keyring = namedKeyring + } + + return &keyring, nil + }) +} + +func (k *keyctlKeyring) Get(name string) (Item, error) { + key, err := keyctlSearch(k.keyring, "user", name) + if err != nil { + if errors.Is(err, syscall.ENOKEY) { + return Item{}, ErrKeyNotFound + } + return Item{}, err + } + // data, err := key.Get() + data, err := keyctlRead(key) + if err != nil { + return Item{}, err + } + + item := Item{ + Key: name, + Data: data, + } + + return item, nil +} + +// GetMetadata for pass returns an error indicating that it's unsupported for this backend. +// TODO: We can deliver metadata different from the defined ones (e.g. permissions, expire-time, etc). +func (k *keyctlKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNotSupported +} + +func (k *keyctlKeyring) Set(item Item) error { + if k.perm == 0 { + // Keep the default permissions (alswrv-----v------------) + _, err := keyctlAdd(k.keyring, "user", item.Key, item.Data) + return err + } + + // By default we loose possession of the key in anything above the session keyring. + // Together with the default permissions (which cannot be changed during creation) we + // cannot change the permissions without possessing the key. Therefore, create the + // key in the session keyring, change permissions and then link to the target + // keyring and unlink from the intermediate keyring again. + key, err := keyctlAdd(unix.KEY_SPEC_SESSION_KEYRING, "user", item.Key, item.Data) + if err != nil { + return fmt.Errorf("adding key to session failed: %v", err) + } + + if err := keyctlSetperm(key, k.perm); err != nil { + return fmt.Errorf("setting permission 0x%x failed: %v", k.perm, err) + } + + if err := keyctlLink(k.keyring, key); err != nil { + return fmt.Errorf("linking key to keyring failed: %v", err) + } + + if err := keyctlUnlink(unix.KEY_SPEC_SESSION_KEYRING, key); err != nil { + return fmt.Errorf("unlinking key from session failed: %v", err) + } + + return nil +} + +func (k *keyctlKeyring) Remove(name string) error { + key, err := keyctlSearch(k.keyring, "user", name) + if err != nil { + return ErrKeyNotFound + } + + return keyctlUnlink(k.keyring, key) +} + +func (k *keyctlKeyring) Keys() ([]string, error) { + results := []string{} + + data, err := keyctlRead(k.keyring) + if err != nil { + return nil, fmt.Errorf("reading keyring failed: %v", err) + } + ids, err := keyctlConvertKeyBuffer(data) + if err != nil { + return nil, fmt.Errorf("converting raw keylist failed: %v", err) + } + + for _, id := range ids { + info, err := keyctlDescribe(id) + if err != nil { + return nil, err + } + if info["type"] == "user" { + results = append(results, info["description"]) + } + } + + return results, nil +} + +func (k *keyctlKeyring) createNamedKeyring(parent int32, name string) (int32, error) { + if k.perm == 0 { + // Keep the default permissions (alswrv-----v------------) + return keyctlAdd(parent, "keyring", name, nil) + } + + // By default we loose possession of the keyring in anything above the session keyring. + // Together with the default permissions (which cannot be changed during creation) we + // cannot change the permissions without possessing the keyring. Therefore, create the + // keyring linked to the session keyring, change permissions and then link to the target + // keyring and unlink from the intermediate keyring again. + keyring, err := keyctlAdd(unix.KEY_SPEC_SESSION_KEYRING, "keyring", name, nil) + if err != nil { + return 0, fmt.Errorf("creating keyring failed: %v", err) + } + + if err := keyctlSetperm(keyring, k.perm); err != nil { + return 0, fmt.Errorf("setting permission 0x%x failed: %v", k.perm, err) + } + + if err := keyctlLink(k.keyring, keyring); err != nil { + return 0, fmt.Errorf("linking keyring failed: %v", err) + } + + if err := keyctlUnlink(unix.KEY_SPEC_SESSION_KEYRING, keyring); err != nil { + return 0, fmt.Errorf("unlinking keyring from session failed: %v", err) + } + + return keyring, nil +} + +func getKeyringForScope(scope string) (int32, error) { + switch scope { + case "user": + return int32(unix.KEY_SPEC_USER_KEYRING), nil + case "usersession": + return int32(unix.KEY_SPEC_USER_SESSION_KEYRING), nil + case "group": + // Not yet implemented in the kernel + // return int32(unix.KEY_SPEC_GROUP_KEYRING) + return 0, fmt.Errorf("scope %q not yet implemented", scope) + case "session": + return int32(unix.KEY_SPEC_SESSION_KEYRING), nil + case "process": + return int32(unix.KEY_SPEC_PROCESS_KEYRING), nil + case "thread": + return int32(unix.KEY_SPEC_THREAD_KEYRING), nil + } + return 0, fmt.Errorf("unknown scope %q", scope) +} + +func keyctlAdd(parent int32, keytype, key string, data []byte) (int32, error) { + id, err := unix.AddKey(keytype, key, data, int(parent)) + if err != nil { + return 0, err + } + return int32(id), nil +} + +func keyctlSearch(id int32, idtype, name string) (int32, error) { + key, err := unix.KeyctlSearch(int(id), idtype, name, 0) + if err != nil { + return 0, err + } + return int32(key), nil +} + +func keyctlRead(id int32) ([]byte, error) { + var buffer []byte + + for { + length, err := unix.KeyctlBuffer(unix.KEYCTL_READ, int(id), buffer, 0) + if err != nil { + return nil, err + } + + // Return the buffer if it was large enough + if length <= len(buffer) { + return buffer[:length], nil + } + + // Next try with a larger buffer + buffer = make([]byte, length) + } +} + +func keyctlDescribe(id int32) (map[string]string, error) { + description, err := unix.KeyctlString(unix.KEYCTL_DESCRIBE, int(id)) + if err != nil { + return nil, err + } + fields := strings.Split(description, ";") + if len(fields) < 1 { + return nil, fmt.Errorf("no data") + } + + data := make(map[string]string) + names := []string{"type", "uid", "gid", "perm"} // according to keyctlDescribe(3) new fields are added at the end + data["description"] = fields[len(fields)-1] // according to keyctlDescribe(3) description is always last + for i, f := range fields[:len(fields)-1] { + if i >= len(names) { + // Do not stumble upon unknown fields + break + } + data[names[i]] = f + } + + return data, nil +} + +func keyctlLink(parent, child int32) error { + _, _, errno := syscall.Syscall(syscall.SYS_KEYCTL, uintptr(unix.KEYCTL_LINK), uintptr(child), uintptr(parent)) + if errno != 0 { + return errno + } + return nil +} + +func keyctlUnlink(parent, child int32) error { + _, _, errno := syscall.Syscall(syscall.SYS_KEYCTL, uintptr(unix.KEYCTL_UNLINK), uintptr(child), uintptr(parent)) + if errno != 0 { + return errno + } + return nil +} + +func keyctlSetperm(id int32, perm uint32) error { + return unix.KeyctlSetperm(int(id), perm) +} + +func keyctlConvertKeyBuffer(buffer []byte) ([]int32, error) { + if len(buffer)%4 != 0 { + return nil, fmt.Errorf("buffer size %d not a multiple of 4", len(buffer)) + } + + results := make([]int32, 0, len(buffer)/4) + for i := 0; i < len(buffer); i += 4 { + // We need to case in host-native endianess here as this is what we get from the kernel. + r := *((*int32)(unsafe.Pointer(&buffer[i]))) + results = append(results, r) + } + return results, nil +} diff --git a/vendor/github.com/99designs/keyring/keyring.go b/vendor/github.com/99designs/keyring/keyring.go new file mode 100644 index 0000000000..12161b7963 --- /dev/null +++ b/vendor/github.com/99designs/keyring/keyring.go @@ -0,0 +1,134 @@ +// Package keyring provides a uniform API over a range of desktop credential storage engines. +package keyring + +import ( + "errors" + "log" + "time" +) + +// BackendType is an identifier for a credential storage service. +type BackendType string + +// All currently supported secure storage backends. +const ( + InvalidBackend BackendType = "" + SecretServiceBackend BackendType = "secret-service" + KeychainBackend BackendType = "keychain" + KeyCtlBackend BackendType = "keyctl" + KWalletBackend BackendType = "kwallet" + WinCredBackend BackendType = "wincred" + FileBackend BackendType = "file" + PassBackend BackendType = "pass" +) + +// This order makes sure the OS-specific backends +// are picked over the more generic backends. +var backendOrder = []BackendType{ + // Windows + WinCredBackend, + // MacOS + KeychainBackend, + // Linux + SecretServiceBackend, + KWalletBackend, + KeyCtlBackend, + // General + PassBackend, + FileBackend, +} + +var supportedBackends = map[BackendType]opener{} + +// AvailableBackends provides a slice of all available backend keys on the current OS. +func AvailableBackends() []BackendType { + b := []BackendType{} + for _, k := range backendOrder { + _, ok := supportedBackends[k] + if ok { + b = append(b, k) + } + } + return b +} + +type opener func(cfg Config) (Keyring, error) + +// Open will open a specific keyring backend. +func Open(cfg Config) (Keyring, error) { + if cfg.AllowedBackends == nil { + cfg.AllowedBackends = AvailableBackends() + } + debugf("Considering backends: %v", cfg.AllowedBackends) + for _, backend := range cfg.AllowedBackends { + if opener, ok := supportedBackends[backend]; ok { + openBackend, err := opener(cfg) + if err != nil { + debugf("Failed backend %s: %s", backend, err) + continue + } + return openBackend, nil + } + } + return nil, ErrNoAvailImpl +} + +// Item is a thing stored on the keyring. +type Item struct { + Key string + Data []byte + Label string + Description string + + // Backend specific config + KeychainNotTrustApplication bool + KeychainNotSynchronizable bool +} + +// Metadata is information about a thing stored on the keyring; retrieving +// metadata must not require authentication. The embedded Item should be +// filled in with an empty Data field. +// It's allowed for Item to be a nil pointer, indicating that all we +// have is the timestamps. +type Metadata struct { + *Item + ModificationTime time.Time +} + +// Keyring provides the uniform interface over the underlying backends. +type Keyring interface { + // Returns an Item matching the key or ErrKeyNotFound + Get(key string) (Item, error) + // Returns the non-secret parts of an Item + GetMetadata(key string) (Metadata, error) + // Stores an Item on the keyring + Set(item Item) error + // Removes the item with matching key + Remove(key string) error + // Provides a slice of all keys stored on the keyring + Keys() ([]string, error) +} + +// ErrNoAvailImpl is returned by Open when a backend cannot be found. +var ErrNoAvailImpl = errors.New("Specified keyring backend not available") + +// ErrKeyNotFound is returned by Keyring Get when the item is not on the keyring. +var ErrKeyNotFound = errors.New("The specified item could not be found in the keyring") + +// ErrMetadataNeedsCredentials is returned when Metadata is called against a +// backend which requires credentials even to see metadata. +var ErrMetadataNeedsCredentials = errors.New("The keyring backend requires credentials for metadata access") + +// ErrMetadataNotSupported is returned when Metadata is not available for the backend. +var ErrMetadataNotSupported = errors.New("The keyring backend does not support metadata access") + +var ( + // Debug specifies whether to print debugging output. + Debug bool +) + +func debugf(pattern string, args ...interface{}) { + if Debug { + log.Printf("[keyring] "+pattern, args...) + } +} diff --git a/vendor/github.com/99designs/keyring/kwallet.go b/vendor/github.com/99designs/keyring/kwallet.go new file mode 100644 index 0000000000..771baba162 --- /dev/null +++ b/vendor/github.com/99designs/keyring/kwallet.go @@ -0,0 +1,237 @@ +//go:build linux +// +build linux + +package keyring + +import ( + "encoding/json" + "os" + + "github.com/godbus/dbus" +) + +const ( + dbusServiceName = "org.kde.kwalletd5" + dbusPath = "/modules/kwalletd5" +) + +func init() { + if os.Getenv("DISABLE_KWALLET") == "1" { + return + } + + // silently fail if dbus isn't available + _, err := dbus.SessionBus() + if err != nil { + return + } + + supportedBackends[KWalletBackend] = opener(func(cfg Config) (Keyring, error) { + if cfg.ServiceName == "" { + cfg.ServiceName = "kdewallet" + } + + if cfg.KWalletAppID == "" { + cfg.KWalletAppID = "keyring" + } + + if cfg.KWalletFolder == "" { + cfg.KWalletFolder = "keyring" + } + + wallet, err := newKwallet() + if err != nil { + return nil, err + } + + ring := &kwalletKeyring{ + wallet: *wallet, + name: cfg.ServiceName, + appID: cfg.KWalletAppID, + folder: cfg.KWalletFolder, + } + + return ring, ring.openWallet() + }) +} + +type kwalletKeyring struct { + wallet kwalletBinding + name string + handle int32 + appID string + folder string +} + +func (k *kwalletKeyring) openWallet() error { + isOpen, err := k.wallet.IsOpen(k.handle) + if err != nil { + return err + } + + if !isOpen { + handle, err := k.wallet.Open(k.name, 0, k.appID) + if err != nil { + return err + } + k.handle = handle + } + + return nil +} + +func (k *kwalletKeyring) Get(key string) (Item, error) { + err := k.openWallet() + if err != nil { + return Item{}, err + } + + data, err := k.wallet.ReadEntry(k.handle, k.folder, key, k.appID) + if err != nil { + return Item{}, err + } + if len(data) == 0 { + return Item{}, ErrKeyNotFound + } + + item := Item{} + err = json.Unmarshal(data, &item) + if err != nil { + return Item{}, err + } + + return item, nil +} + +// GetMetadata for kwallet returns an error indicating that it's unsupported +// for this backend. +// +// The only APIs found around KWallet are for retrieving content, no indication +// found in docs for methods to use to retrieve metadata without needing unlock +// credentials. +func (k *kwalletKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNeedsCredentials +} + +func (k *kwalletKeyring) Set(item Item) error { + err := k.openWallet() + if err != nil { + return err + } + + data, err := json.Marshal(item) + if err != nil { + return err + } + + err = k.wallet.WriteEntry(k.handle, k.folder, item.Key, data, k.appID) + if err != nil { + return err + } + + return nil +} + +func (k *kwalletKeyring) Remove(key string) error { + err := k.openWallet() + if err != nil { + return err + } + + err = k.wallet.RemoveEntry(k.handle, k.folder, key, k.appID) + if err != nil { + return err + } + + return nil +} + +func (k *kwalletKeyring) Keys() ([]string, error) { + err := k.openWallet() + if err != nil { + return []string{}, err + } + + entries, err := k.wallet.EntryList(k.handle, k.folder, k.appID) + if err != nil { + return []string{}, err + } + + return entries, nil +} + +func newKwallet() (*kwalletBinding, error) { + conn, err := dbus.SessionBus() + if err != nil { + return nil, err + } + + return &kwalletBinding{ + conn.Object(dbusServiceName, dbusPath), + }, nil +} + +// Dumb Dbus bindings for kwallet bindings with types. +type kwalletBinding struct { + dbus dbus.BusObject +} + +// method bool org.kde.KWallet.isOpen(int handle) +func (k *kwalletBinding) IsOpen(handle int32) (bool, error) { + call := k.dbus.Call("org.kde.KWallet.isOpen", 0, handle) + if call.Err != nil { + return false, call.Err + } + + return call.Body[0].(bool), call.Err +} + +// method int org.kde.KWallet.open(QString wallet, qlonglong wId, QString appid) +func (k *kwalletBinding) Open(name string, wID int64, appid string) (int32, error) { + call := k.dbus.Call("org.kde.KWallet.open", 0, name, wID, appid) + if call.Err != nil { + return 0, call.Err + } + + return call.Body[0].(int32), call.Err +} + +// method QStringList org.kde.KWallet.entryList(int handle, QString folder, QString appid) +func (k *kwalletBinding) EntryList(handle int32, folder string, appid string) ([]string, error) { + call := k.dbus.Call("org.kde.KWallet.entryList", 0, handle, folder, appid) + if call.Err != nil { + return []string{}, call.Err + } + + return call.Body[0].([]string), call.Err +} + +// method int org.kde.KWallet.writeEntry(int handle, QString folder, QString key, QByteArray value, QString appid) +func (k *kwalletBinding) WriteEntry(handle int32, folder string, key string, value []byte, appid string) error { + call := k.dbus.Call("org.kde.KWallet.writeEntry", 0, handle, folder, key, value, appid) + if call.Err != nil { + return call.Err + } + + return call.Err +} + +// method int org.kde.KWallet.removeEntry(int handle, QString folder, QString key, QString appid) +func (k *kwalletBinding) RemoveEntry(handle int32, folder string, key string, appid string) error { + call := k.dbus.Call("org.kde.KWallet.removeEntry", 0, handle, folder, key, appid) + if call.Err != nil { + return call.Err + } + + return call.Err +} + +// method QByteArray org.kde.KWallet.readEntry(int handle, QString folder, QString key, QString appid) +func (k *kwalletBinding) ReadEntry(handle int32, folder string, key string, appid string) ([]byte, error) { + call := k.dbus.Call("org.kde.KWallet.readEntry", 0, handle, folder, key, appid) + if call.Err != nil { + return []byte{}, call.Err + } + + return call.Body[0].([]byte), call.Err +} diff --git a/vendor/github.com/99designs/keyring/pass.go b/vendor/github.com/99designs/keyring/pass.go new file mode 100644 index 0000000000..5ca0bf0f51 --- /dev/null +++ b/vendor/github.com/99designs/keyring/pass.go @@ -0,0 +1,166 @@ +//go:build !windows +// +build !windows + +package keyring + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func init() { + supportedBackends[PassBackend] = opener(func(cfg Config) (Keyring, error) { + var err error + + pass := &passKeyring{ + passcmd: cfg.PassCmd, + dir: cfg.PassDir, + prefix: cfg.PassPrefix, + } + + if pass.passcmd == "" { + pass.passcmd = "pass" + } + + if pass.dir == "" { + if passDir, found := os.LookupEnv("PASSWORD_STORE_DIR"); found { + pass.dir = passDir + } else { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, err + } + pass.dir = filepath.Join(homeDir, ".password-store") + } + } + + pass.dir, err = ExpandTilde(pass.dir) + if err != nil { + return nil, err + } + + // fail if the pass program is not available + _, err = exec.LookPath(pass.passcmd) + if err != nil { + return nil, errors.New("The pass program is not available") + } + + return pass, nil + }) +} + +type passKeyring struct { + dir string + passcmd string + prefix string +} + +func (k *passKeyring) pass(args ...string) *exec.Cmd { + cmd := exec.Command(k.passcmd, args...) + if k.dir != "" { + cmd.Env = append(os.Environ(), fmt.Sprintf("PASSWORD_STORE_DIR=%s", k.dir)) + } + cmd.Stderr = os.Stderr + + return cmd +} + +func (k *passKeyring) Get(key string) (Item, error) { + if !k.itemExists(key) { + return Item{}, ErrKeyNotFound + } + + name := filepath.Join(k.prefix, key) + cmd := k.pass("show", name) + output, err := cmd.Output() + if err != nil { + return Item{}, err + } + + var decoded Item + err = json.Unmarshal(output, &decoded) + + return decoded, err +} + +func (k *passKeyring) GetMetadata(key string) (Metadata, error) { + return Metadata{}, nil +} + +func (k *passKeyring) Set(i Item) error { + bytes, err := json.Marshal(i) + if err != nil { + return err + } + + name := filepath.Join(k.prefix, i.Key) + cmd := k.pass("insert", "-m", "-f", name) + cmd.Stdin = strings.NewReader(string(bytes)) + + err = cmd.Run() + if err != nil { + return err + } + + return nil +} + +func (k *passKeyring) Remove(key string) error { + if !k.itemExists(key) { + return ErrKeyNotFound + } + + name := filepath.Join(k.prefix, key) + cmd := k.pass("rm", "-f", name) + err := cmd.Run() + if err != nil { + return err + } + + return nil +} + +func (k *passKeyring) itemExists(key string) bool { + var path = filepath.Join(k.dir, k.prefix, key+".gpg") + _, err := os.Stat(path) + + return err == nil +} + +func (k *passKeyring) Keys() ([]string, error) { + var keys = []string{} + var path = filepath.Join(k.dir, k.prefix) + + info, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + return keys, nil + } + return keys, err + } + if !info.IsDir() { + return keys, fmt.Errorf("%s is not a directory", path) + } + + err = filepath.Walk(path, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() && filepath.Ext(p) == ".gpg" { + name := strings.TrimPrefix(p, path) + if name[0] == os.PathSeparator { + name = name[1:] + } + keys = append(keys, name[:len(name)-4]) + } + return nil + }) + + return keys, err +} diff --git a/vendor/github.com/99designs/keyring/prompt.go b/vendor/github.com/99designs/keyring/prompt.go new file mode 100644 index 0000000000..59ad81c352 --- /dev/null +++ b/vendor/github.com/99designs/keyring/prompt.go @@ -0,0 +1,27 @@ +package keyring + +import ( + "fmt" + "os" + + "golang.org/x/term" +) + +// PromptFunc is a function used to prompt the user for a password. +type PromptFunc func(string) (string, error) + +func TerminalPrompt(prompt string) (string, error) { + fmt.Printf("%s: ", prompt) + b, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + return "", err + } + fmt.Println() + return string(b), nil +} + +func FixedStringPrompt(value string) PromptFunc { + return func(_ string) (string, error) { + return value, nil + } +} diff --git a/vendor/github.com/99designs/keyring/secretservice.go b/vendor/github.com/99designs/keyring/secretservice.go new file mode 100644 index 0000000000..2a1f9d99f1 --- /dev/null +++ b/vendor/github.com/99designs/keyring/secretservice.go @@ -0,0 +1,293 @@ +//go:build linux +// +build linux + +package keyring + +import ( + "encoding/hex" + "encoding/json" + "errors" + + "strings" + + "github.com/godbus/dbus" + "github.com/gsterjov/go-libsecret" +) + +func init() { + // silently fail if dbus isn't available + _, err := dbus.SessionBus() + if err != nil { + return + } + + supportedBackends[SecretServiceBackend] = opener(func(cfg Config) (Keyring, error) { + if cfg.ServiceName == "" { + cfg.ServiceName = "secret-service" + } + if cfg.LibSecretCollectionName == "" { + cfg.LibSecretCollectionName = cfg.ServiceName + } + + service, err := libsecret.NewService() + if err != nil { + return &secretsKeyring{}, err + } + + ring := &secretsKeyring{ + name: cfg.LibSecretCollectionName, + service: service, + } + + return ring, ring.openSecrets() + }) +} + +type secretsKeyring struct { + name string + service *libsecret.Service + collection *libsecret.Collection + session *libsecret.Session +} + +var errCollectionNotFound = errors.New("The collection does not exist. Please add a key first") + +func decodeKeyringString(src string) string { + var dst strings.Builder + for i := 0; i < len(src); i++ { + if src[i] != '_' { + dst.WriteString(string(src[i])) + } else { + if i+3 > len(src) { + return src + } + hexstring := src[i+1 : i+3] + decoded, err := hex.DecodeString(hexstring) + if err != nil { + return src + } + dst.Write(decoded) + i += 2 + } + } + return dst.String() +} + +func (k *secretsKeyring) openSecrets() error { + session, err := k.service.Open() + if err != nil { + return err + } + k.session = session + + // get the collection if it already exists + collections, err := k.service.Collections() + if err != nil { + return err + } + + path := libsecret.DBusPath + "/collection/" + k.name + + for _, collection := range collections { + if decodeKeyringString(string(collection.Path())) == path { + c := collection // fix variable into the local variable to ensure it's referenced correctly, see https://github.com/kyoh86/exportloopref + k.collection = &c + return nil + } + } + + return nil +} + +func (k *secretsKeyring) openCollection() error { + if err := k.openSecrets(); err != nil { + return err + } + + if k.collection == nil { + return errCollectionNotFound + // return &secretsError{fmt.Sprintf( + // "The collection %q does not exist. Please add a key first", + // k.name, + // )} + } + + return nil +} + +func (k *secretsKeyring) Get(key string) (Item, error) { + if err := k.openCollection(); err != nil { + if err == errCollectionNotFound { + return Item{}, ErrKeyNotFound + } + return Item{}, err + } + + items, err := k.collection.SearchItems(key) + if err != nil { + return Item{}, err + } + + if len(items) == 0 { + return Item{}, ErrKeyNotFound + } + + // use the first item whenever there are multiples + // with the same profile name + item := items[0] + + locked, err := item.Locked() + if err != nil { + return Item{}, err + } + + if locked { + if err := k.service.Unlock(item); err != nil { + return Item{}, err + } + } + + secret, err := item.GetSecret(k.session) + if err != nil { + return Item{}, err + } + + // pack the secret into the item + var ret Item + if err = json.Unmarshal(secret.Value, &ret); err != nil { + return Item{}, err + } + + return ret, err +} + +// GetMetadata for libsecret returns an error indicating that it's unsupported +// for this backend. +// +// libsecret actually implements a metadata system which we could use, "Secret +// Attributes"; I found no indication in documentation of anything like an +// automatically maintained last-modification timestamp, so to use this we'd +// need to have a SetMetadata API too. Which we're not yet doing, but feel +// free to contribute patches. +func (k *secretsKeyring) GetMetadata(key string) (Metadata, error) { + return Metadata{}, ErrMetadataNeedsCredentials +} + +func (k *secretsKeyring) Set(item Item) error { + err := k.openSecrets() + if err != nil { + return err + } + + // create the collection if it doesn't already exist + if k.collection == nil { + collection, err := k.service.CreateCollection(k.name) + if err != nil { + return err + } + + k.collection = collection + } + + if err := k.ensureCollectionUnlocked(); err != nil { + return err + } + + // create the new item + data, err := json.Marshal(item) + if err != nil { + return err + } + + secret := libsecret.NewSecret(k.session, []byte{}, data, "application/json") + + if _, err := k.collection.CreateItem(item.Key, secret, true); err != nil { + return err + } + + return nil +} + +func (k *secretsKeyring) Remove(key string) error { + if err := k.openCollection(); err != nil { + if err == errCollectionNotFound { + return ErrKeyNotFound + } + return err + } + + items, err := k.collection.SearchItems(key) + if err != nil { + return err + } + + // nothing to delete + if len(items) == 0 { + return nil + } + + // we dont want to delete more than one anyway + // so just get the first item found + item := items[0] + + locked, err := item.Locked() + if err != nil { + return err + } + + if locked { + if err := k.service.Unlock(item); err != nil { + return err + } + } + + if err := item.Delete(); err != nil { + return err + } + + return nil +} + +func (k *secretsKeyring) Keys() ([]string, error) { + if err := k.openCollection(); err != nil { + if err == errCollectionNotFound { + return []string{}, nil + } + return nil, err + } + if err := k.ensureCollectionUnlocked(); err != nil { + return nil, err + } + items, err := k.collection.Items() + if err != nil { + return nil, err + } + keys := []string{} + for _, item := range items { + label, err := item.Label() // FIXME: err is being silently ignored + if err == nil { + keys = append(keys, label) + } + } + return keys, nil +} + +// deleteCollection deletes the keyring's collection if it exists. This is mainly to support testing. +func (k *secretsKeyring) deleteCollection() error { + if err := k.openCollection(); err != nil { + return err + } + return k.collection.Delete() +} + +// unlock the collection if it's locked +func (k *secretsKeyring) ensureCollectionUnlocked() error { + locked, err := k.collection.Locked() + if err != nil { + return err + } + if !locked { + return nil + } + return k.service.Unlock(k.collection) +} diff --git a/vendor/github.com/99designs/keyring/tilde.go b/vendor/github.com/99designs/keyring/tilde.go new file mode 100644 index 0000000000..c1847a61bf --- /dev/null +++ b/vendor/github.com/99designs/keyring/tilde.go @@ -0,0 +1,22 @@ +package keyring + +import ( + "os" + "path/filepath" + "strings" +) + +var tildePrefix = string([]rune{'~', filepath.Separator}) + +// ExpandTilde will expand tilde (~/ or ~\ depending on OS) for the user home directory. +func ExpandTilde(dir string) (string, error) { + if strings.HasPrefix(dir, tildePrefix) { + homeDir, err := os.UserHomeDir() + if err != nil { + return "", err + } + dir = strings.Replace(dir, "~", homeDir, 1) + debugf("Expanded file dir to %s", dir) + } + return dir, nil +} diff --git a/vendor/github.com/99designs/keyring/wincred.go b/vendor/github.com/99designs/keyring/wincred.go new file mode 100644 index 0000000000..2ed43f2f6d --- /dev/null +++ b/vendor/github.com/99designs/keyring/wincred.go @@ -0,0 +1,98 @@ +//go:build windows +// +build windows + +package keyring + +import ( + "strings" + "syscall" + + "github.com/danieljoos/wincred" +) + +// ERROR_NOT_FOUND from https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--1000-1299- +const elementNotFoundError = syscall.Errno(1168) + +type windowsKeyring struct { + name string + prefix string +} + +func init() { + supportedBackends[WinCredBackend] = opener(func(cfg Config) (Keyring, error) { + name := cfg.ServiceName + if name == "" { + name = "default" + } + + prefix := cfg.WinCredPrefix + if prefix == "" { + prefix = "keyring" + } + + return &windowsKeyring{ + name: name, + prefix: prefix, + }, nil + }) +} + +func (k *windowsKeyring) Get(key string) (Item, error) { + cred, err := wincred.GetGenericCredential(k.credentialName(key)) + if err != nil { + if err == elementNotFoundError { + return Item{}, ErrKeyNotFound + } + return Item{}, err + } + + item := Item{ + Key: key, + Data: cred.CredentialBlob, + } + + return item, nil +} + +// GetMetadata for pass returns an error indicating that it's unsupported +// for this backend. +// TODO: This is a stub. Look into whether pass would support metadata in a usable way for keyring. +func (k *windowsKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNotSupported +} + +func (k *windowsKeyring) Set(item Item) error { + cred := wincred.NewGenericCredential(k.credentialName(item.Key)) + cred.CredentialBlob = item.Data + return cred.Write() +} + +func (k *windowsKeyring) Remove(key string) error { + cred, err := wincred.GetGenericCredential(k.credentialName(key)) + if err != nil { + if err == elementNotFoundError { + return ErrKeyNotFound + } + return err + } + return cred.Delete() +} + +func (k *windowsKeyring) Keys() ([]string, error) { + results := []string{} + + if creds, err := wincred.List(); err == nil { + for _, cred := range creds { + prefix := k.credentialName("") + if strings.HasPrefix(cred.TargetName, prefix) { + results = append(results, strings.TrimPrefix(cred.TargetName, prefix)) + } + } + } + + return results, nil +} + +func (k *windowsKeyring) credentialName(key string) string { + return k.prefix + ":" + k.name + ":" + key +} diff --git a/vendor/github.com/alessio/shellescape/.gitignore b/vendor/github.com/alessio/shellescape/.gitignore new file mode 100644 index 0000000000..4ba7c2d137 --- /dev/null +++ b/vendor/github.com/alessio/shellescape/.gitignore @@ -0,0 +1,28 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.idea/ + +escargs diff --git a/vendor/github.com/alessio/shellescape/.golangci.yml b/vendor/github.com/alessio/shellescape/.golangci.yml new file mode 100644 index 0000000000..cd4a17e442 --- /dev/null +++ b/vendor/github.com/alessio/shellescape/.golangci.yml @@ -0,0 +1,64 @@ +# run: +# # timeout for analysis, e.g. 30s, 5m, default is 1m +# timeout: 5m + +linters: + disable-all: true + enable: + - bodyclose + - deadcode + - depguard + - dogsled + - goconst + - gocritic + - gofmt + - goimports + - golint + - gosec + - gosimple + - govet + - ineffassign + - interfacer + - maligned + - misspell + - prealloc + - scopelint + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - misspell + - wsl + +issues: + exclude-rules: + - text: "Use of weak random number generator" + linters: + - gosec + - text: "comment on exported var" + linters: + - golint + - text: "don't use an underscore in package name" + linters: + - golint + - text: "ST1003:" + linters: + - stylecheck + # FIXME: Disabled until golangci-lint updates stylecheck with this fix: + # https://github.com/dominikh/go-tools/issues/389 + - text: "ST1016:" + linters: + - stylecheck + +linters-settings: + dogsled: + max-blank-identifiers: 3 + maligned: + # print struct with more effective memory layout or not, false by default + suggest-new: true + +run: + tests: false diff --git a/vendor/github.com/alessio/shellescape/.goreleaser.yml b/vendor/github.com/alessio/shellescape/.goreleaser.yml new file mode 100644 index 0000000000..064c9374d7 --- /dev/null +++ b/vendor/github.com/alessio/shellescape/.goreleaser.yml @@ -0,0 +1,33 @@ +# This is an example goreleaser.yaml file with some sane defaults. +# Make sure to check the documentation at http://goreleaser.com +before: + hooks: + # You may remove this if you don't use go modules. + - go mod download + # you may remove this if you don't need go generate + - go generate ./... +builds: + - env: + - CGO_ENABLED=0 + main: ./cmd/escargs + goos: + - linux + - windows + - darwin +archives: + - replacements: + darwin: Darwin + linux: Linux + windows: Windows + 386: i386 + amd64: x86_64 +checksum: + name_template: 'checksums.txt' +snapshot: + name_template: "{{ .Tag }}-next" +changelog: + sort: asc + filters: + exclude: + - '^docs:' + - '^test:' diff --git a/vendor/github.com/alessio/shellescape/AUTHORS b/vendor/github.com/alessio/shellescape/AUTHORS new file mode 100644 index 0000000000..4a647a6f41 --- /dev/null +++ b/vendor/github.com/alessio/shellescape/AUTHORS @@ -0,0 +1 @@ +Alessio Treglia diff --git a/vendor/github.com/alessio/shellescape/CODE_OF_CONDUCT.md b/vendor/github.com/alessio/shellescape/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000..e8eda60620 --- /dev/null +++ b/vendor/github.com/alessio/shellescape/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at alessio@debian.org. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/vendor/github.com/alessio/shellescape/LICENSE b/vendor/github.com/alessio/shellescape/LICENSE new file mode 100644 index 0000000000..9f760679f4 --- /dev/null +++ b/vendor/github.com/alessio/shellescape/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Alessio Treglia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/alessio/shellescape/README.md b/vendor/github.com/alessio/shellescape/README.md new file mode 100644 index 0000000000..910bb253b9 --- /dev/null +++ b/vendor/github.com/alessio/shellescape/README.md @@ -0,0 +1,61 @@ +![Build](https://github.com/alessio/shellescape/workflows/Build/badge.svg) +[![GoDoc](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/alessio/shellescape?tab=overview) +[![sourcegraph](https://sourcegraph.com/github.com/alessio/shellescape/-/badge.svg)](https://sourcegraph.com/github.com/alessio/shellescape) +[![codecov](https://codecov.io/gh/alessio/shellescape/branch/master/graph/badge.svg)](https://codecov.io/gh/alessio/shellescape) +[![Coverage](https://gocover.io/_badge/github.com/alessio/shellescape)](https://gocover.io/github.com/alessio/shellescape) +[![Go Report Card](https://goreportcard.com/badge/github.com/alessio/shellescape)](https://goreportcard.com/report/github.com/alessio/shellescape) + +# shellescape +Escape arbitrary strings for safe use as command line arguments. +## Contents of the package + +This package provides the `shellescape.Quote()` function that returns a +shell-escaped copy of a string. This functionality could be helpful +in those cases where it is known that the output of a Go program will +be appended to/used in the context of shell programs' command line arguments. + +This work was inspired by the Python original package +[shellescape](https://pypi.python.org/pypi/shellescape). + +## Usage + +The following snippet shows a typical unsafe idiom: + +```go +package main + +import ( + "fmt" + "os" +) + +func main() { + fmt.Printf("ls -l %s\n", os.Args[1]) +} +``` +_[See in Go Playground](https://play.golang.org/p/Wj2WoUfH_d)_ + +Especially when creating pipeline of commands which might end up being +executed by a shell interpreter, it is particularly unsafe to not +escape arguments. + +`shellescape.Quote()` comes in handy and to safely escape strings: + +```go +package main + +import ( + "fmt" + "os" + + "gopkg.in/alessio/shellescape.v1" +) + +func main() { + fmt.Printf("ls -l %s\n", shellescape.Quote(os.Args[1])) +} +``` +_[See in Go Playground](https://play.golang.org/p/HJ_CXgSrmp)_ + +## The escargs utility +__escargs__ reads lines from the standard input and prints shell-escaped versions. Unlinke __xargs__, blank lines on the standard input are not discarded. diff --git a/vendor/github.com/alessio/shellescape/shellescape.go b/vendor/github.com/alessio/shellescape/shellescape.go new file mode 100644 index 0000000000..dc34a556ae --- /dev/null +++ b/vendor/github.com/alessio/shellescape/shellescape.go @@ -0,0 +1,66 @@ +/* +Package shellescape provides the shellescape.Quote to escape arbitrary +strings for a safe use as command line arguments in the most common +POSIX shells. + +The original Python package which this work was inspired by can be found +at https://pypi.python.org/pypi/shellescape. +*/ +package shellescape // "import gopkg.in/alessio/shellescape.v1" + +/* +The functionality provided by shellescape.Quote could be helpful +in those cases where it is known that the output of a Go program will +be appended to/used in the context of shell programs' command line arguments. +*/ + +import ( + "regexp" + "strings" + "unicode" +) + +var pattern *regexp.Regexp + +func init() { + pattern = regexp.MustCompile(`[^\w@%+=:,./-]`) +} + +// Quote returns a shell-escaped version of the string s. The returned value +// is a string that can safely be used as one token in a shell command line. +func Quote(s string) string { + if len(s) == 0 { + return "''" + } + + if pattern.MatchString(s) { + return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'" + } + + return s +} + +// QuoteCommand returns a shell-escaped version of the slice of strings. +// The returned value is a string that can safely be used as shell command arguments. +func QuoteCommand(args []string) string { + l := make([]string, len(args)) + + for i, s := range args { + l[i] = Quote(s) + } + + return strings.Join(l, " ") +} + +// StripUnsafe remove non-printable runes, e.g. control characters in +// a string that is meant for consumption by terminals that support +// control characters. +func StripUnsafe(s string) string { + return strings.Map(func(r rune) rune { + if unicode.IsPrint(r) { + return r + } + + return -1 + }, s) +} diff --git a/vendor/github.com/danieljoos/wincred/.gitattributes b/vendor/github.com/danieljoos/wincred/.gitattributes new file mode 100644 index 0000000000..d207b1802b --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/.gitattributes @@ -0,0 +1 @@ +*.go text eol=lf diff --git a/vendor/github.com/danieljoos/wincred/.gitignore b/vendor/github.com/danieljoos/wincred/.gitignore new file mode 100644 index 0000000000..6142c06916 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/.gitignore @@ -0,0 +1,25 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test + +coverage.txt diff --git a/vendor/github.com/danieljoos/wincred/LICENSE b/vendor/github.com/danieljoos/wincred/LICENSE new file mode 100644 index 0000000000..2f436f1b30 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Daniel Joos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/danieljoos/wincred/README.md b/vendor/github.com/danieljoos/wincred/README.md new file mode 100644 index 0000000000..8a879b0cef --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/README.md @@ -0,0 +1,145 @@ +wincred +======= + +Go wrapper around the Windows Credential Manager API functions. + +[![GitHub release](https://img.shields.io/github/release/danieljoos/wincred.svg?style=flat-square)](https://github.com/danieljoos/wincred/releases/latest) +[![Test Status](https://img.shields.io/github/actions/workflow/status/danieljoos/wincred/test.yml?label=test&logo=github&style=flat-square)](https://github.com/danieljoos/wincred/actions?query=workflow%3Atest) +[![Go Report Card](https://goreportcard.com/badge/github.com/danieljoos/wincred)](https://goreportcard.com/report/github.com/danieljoos/wincred) +[![Codecov](https://img.shields.io/codecov/c/github/danieljoos/wincred?logo=codecov&style=flat-square)](https://codecov.io/gh/danieljoos/wincred) +[![PkgGoDev](https://img.shields.io/badge/go.dev-docs-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/github.com/danieljoos/wincred) + +Installation +------------ + +```Go +go get github.com/danieljoos/wincred +``` + + +Usage +----- + +See the following examples: + +### Create and store a new generic credential object +```Go +package main + +import ( + "fmt" + "github.com/danieljoos/wincred" +) + +func main() { + cred := wincred.NewGenericCredential("myGoApplication") + cred.CredentialBlob = []byte("my secret") + err := cred.Write() + + if err != nil { + fmt.Println(err) + } +} +``` + +### Retrieve a credential object +```Go +package main + +import ( + "fmt" + "github.com/danieljoos/wincred" +) + +func main() { + cred, err := wincred.GetGenericCredential("myGoApplication") + if err == nil { + fmt.Println(string(cred.CredentialBlob)) + } +} +``` + +### Remove a credential object +```Go +package main + +import ( + "fmt" + "github.com/danieljoos/wincred" +) + +func main() { + cred, err := wincred.GetGenericCredential("myGoApplication") + if err != nil { + fmt.Println(err) + return + } + cred.Delete() +} +``` + +### List all available credentials +```Go +package main + +import ( + "fmt" + "github.com/danieljoos/wincred" +) + +func main() { + creds, err := wincred.List() + if err != nil { + fmt.Println(err) + return + } + for i := range(creds) { + fmt.Println(creds[i].TargetName) + } +} +``` + +Hints +----- + +### Encoding + +The credential objects simply store byte arrays without specific meaning or encoding. +For sharing between different applications, it might make sense to apply an explicit string encoding - for example **UTF-16 LE** (used nearly everywhere in the Win32 API). + +```Go +package main + +import ( + "fmt" + "os" + + "github.com/danieljoos/wincred" + "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" +) + +func main() { + cred := wincred.NewGenericCredential("myGoApplication") + + encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder() + blob, _, err := transform.Bytes(encoder, []byte("mysecret")) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + cred.CredentialBlob = blob + err = cred.Write() + + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +``` + +### Limitations + +The size of a credential blob is limited to **2560 Bytes** by the Windows API. diff --git a/vendor/github.com/danieljoos/wincred/conversion.go b/vendor/github.com/danieljoos/wincred/conversion.go new file mode 100644 index 0000000000..bc04f50f1f --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/conversion.go @@ -0,0 +1,116 @@ +// +build windows + +package wincred + +import ( + "encoding/binary" + "reflect" + "time" + "unsafe" + + syscall "golang.org/x/sys/windows" +) + +// utf16ToByte creates a byte array from a given UTF 16 char array. +func utf16ToByte(wstr []uint16) (result []byte) { + result = make([]byte, len(wstr)*2) + for i := range wstr { + binary.LittleEndian.PutUint16(result[(i*2):(i*2)+2], wstr[i]) + } + return +} + +// utf16FromString creates a UTF16 char array from a string. +func utf16FromString(str string) []uint16 { + res, err := syscall.UTF16FromString(str) + if err != nil { + return []uint16{} + } + return res +} + +// goBytes copies the given C byte array to a Go byte array (see `C.GoBytes`). +// This function avoids having cgo as dependency. +func goBytes(src uintptr, len uint32) []byte { + if src == uintptr(0) { + return []byte{} + } + rv := make([]byte, len) + copy(rv, *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ + Data: src, + Len: int(len), + Cap: int(len), + }))) + return rv +} + +// Convert the given CREDENTIAL struct to a more usable structure +func sysToCredential(cred *sysCREDENTIAL) (result *Credential) { + if cred == nil { + return nil + } + result = new(Credential) + result.Comment = syscall.UTF16PtrToString(cred.Comment) + result.TargetName = syscall.UTF16PtrToString(cred.TargetName) + result.TargetAlias = syscall.UTF16PtrToString(cred.TargetAlias) + result.UserName = syscall.UTF16PtrToString(cred.UserName) + result.LastWritten = time.Unix(0, cred.LastWritten.Nanoseconds()) + result.Persist = CredentialPersistence(cred.Persist) + result.CredentialBlob = goBytes(cred.CredentialBlob, cred.CredentialBlobSize) + result.Attributes = make([]CredentialAttribute, cred.AttributeCount) + attrSlice := *(*[]sysCREDENTIAL_ATTRIBUTE)(unsafe.Pointer(&reflect.SliceHeader{ + Data: cred.Attributes, + Len: int(cred.AttributeCount), + Cap: int(cred.AttributeCount), + })) + for i, attr := range attrSlice { + resultAttr := &result.Attributes[i] + resultAttr.Keyword = syscall.UTF16PtrToString(attr.Keyword) + resultAttr.Value = goBytes(attr.Value, attr.ValueSize) + } + return result +} + +// Convert the given Credential object back to a CREDENTIAL struct, which can be used for calling the +// Windows APIs +func sysFromCredential(cred *Credential) (result *sysCREDENTIAL) { + if cred == nil { + return nil + } + result = new(sysCREDENTIAL) + result.Flags = 0 + result.Type = 0 + result.TargetName, _ = syscall.UTF16PtrFromString(cred.TargetName) + result.Comment, _ = syscall.UTF16PtrFromString(cred.Comment) + result.LastWritten = syscall.NsecToFiletime(cred.LastWritten.UnixNano()) + result.CredentialBlobSize = uint32(len(cred.CredentialBlob)) + if len(cred.CredentialBlob) > 0 { + result.CredentialBlob = uintptr(unsafe.Pointer(&cred.CredentialBlob[0])) + } else { + result.CredentialBlob = 0 + } + result.Persist = uint32(cred.Persist) + result.AttributeCount = uint32(len(cred.Attributes)) + attributes := make([]sysCREDENTIAL_ATTRIBUTE, len(cred.Attributes)) + if len(attributes) > 0 { + result.Attributes = uintptr(unsafe.Pointer(&attributes[0])) + } else { + result.Attributes = 0 + } + for i := range cred.Attributes { + inAttr := &cred.Attributes[i] + outAttr := &attributes[i] + outAttr.Keyword, _ = syscall.UTF16PtrFromString(inAttr.Keyword) + outAttr.Flags = 0 + outAttr.ValueSize = uint32(len(inAttr.Value)) + if len(inAttr.Value) > 0 { + outAttr.Value = uintptr(unsafe.Pointer(&inAttr.Value[0])) + } else { + outAttr.Value = 0 + } + } + result.TargetAlias, _ = syscall.UTF16PtrFromString(cred.TargetAlias) + result.UserName, _ = syscall.UTF16PtrFromString(cred.UserName) + + return +} diff --git a/vendor/github.com/danieljoos/wincred/conversion_unsupported.go b/vendor/github.com/danieljoos/wincred/conversion_unsupported.go new file mode 100644 index 0000000000..a1ea720750 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/conversion_unsupported.go @@ -0,0 +1,11 @@ +// +build !windows + +package wincred + +func utf16ToByte(...interface{}) []byte { + return nil +} + +func utf16FromString(...interface{}) []uint16 { + return nil +} diff --git a/vendor/github.com/danieljoos/wincred/sys.go b/vendor/github.com/danieljoos/wincred/sys.go new file mode 100644 index 0000000000..dfff59624b --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/sys.go @@ -0,0 +1,147 @@ +//go:build windows +// +build windows + +package wincred + +import ( + "reflect" + "syscall" + "unsafe" + + "golang.org/x/sys/windows" +) + +var ( + modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") + procCredRead = modadvapi32.NewProc("CredReadW") + procCredWrite proc = modadvapi32.NewProc("CredWriteW") + procCredDelete proc = modadvapi32.NewProc("CredDeleteW") + procCredFree proc = modadvapi32.NewProc("CredFree") + procCredEnumerate = modadvapi32.NewProc("CredEnumerateW") +) + +// Interface for syscall.Proc: helps testing +type proc interface { + Call(a ...uintptr) (r1, r2 uintptr, lastErr error) +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw +type sysCREDENTIAL struct { + Flags uint32 + Type uint32 + TargetName *uint16 + Comment *uint16 + LastWritten windows.Filetime + CredentialBlobSize uint32 + CredentialBlob uintptr + Persist uint32 + AttributeCount uint32 + Attributes uintptr + TargetAlias *uint16 + UserName *uint16 +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credential_attributew +type sysCREDENTIAL_ATTRIBUTE struct { + Keyword *uint16 + Flags uint32 + ValueSize uint32 + Value uintptr +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw +type sysCRED_TYPE uint32 + +const ( + sysCRED_TYPE_GENERIC sysCRED_TYPE = 0x1 + sysCRED_TYPE_DOMAIN_PASSWORD sysCRED_TYPE = 0x2 + sysCRED_TYPE_DOMAIN_CERTIFICATE sysCRED_TYPE = 0x3 + sysCRED_TYPE_DOMAIN_VISIBLE_PASSWORD sysCRED_TYPE = 0x4 + sysCRED_TYPE_GENERIC_CERTIFICATE sysCRED_TYPE = 0x5 + sysCRED_TYPE_DOMAIN_EXTENDED sysCRED_TYPE = 0x6 + + // https://docs.microsoft.com/en-us/windows/desktop/Debug/system-error-codes + sysERROR_NOT_FOUND = windows.Errno(1168) + sysERROR_INVALID_PARAMETER = windows.Errno(87) +) + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credreadw +func sysCredRead(targetName string, typ sysCRED_TYPE) (*Credential, error) { + var pcred *sysCREDENTIAL + targetNamePtr, _ := windows.UTF16PtrFromString(targetName) + ret, _, err := syscall.SyscallN( + procCredRead.Addr(), + uintptr(unsafe.Pointer(targetNamePtr)), + uintptr(typ), + 0, + uintptr(unsafe.Pointer(&pcred)), + ) + if ret == 0 { + return nil, err + } + defer procCredFree.Call(uintptr(unsafe.Pointer(pcred))) + + return sysToCredential(pcred), nil +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credwritew +func sysCredWrite(cred *Credential, typ sysCRED_TYPE) error { + ncred := sysFromCredential(cred) + ncred.Type = uint32(typ) + ret, _, err := procCredWrite.Call( + uintptr(unsafe.Pointer(ncred)), + 0, + ) + if ret == 0 { + return err + } + + return nil +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-creddeletew +func sysCredDelete(cred *Credential, typ sysCRED_TYPE) error { + targetNamePtr, _ := windows.UTF16PtrFromString(cred.TargetName) + ret, _, err := procCredDelete.Call( + uintptr(unsafe.Pointer(targetNamePtr)), + uintptr(typ), + 0, + ) + if ret == 0 { + return err + } + + return nil +} + +// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credenumeratew +func sysCredEnumerate(filter string, all bool) ([]*Credential, error) { + var count int + var pcreds uintptr + var filterPtr *uint16 + if !all { + filterPtr, _ = windows.UTF16PtrFromString(filter) + } + ret, _, err := syscall.SyscallN( + procCredEnumerate.Addr(), + uintptr(unsafe.Pointer(filterPtr)), + 0, + uintptr(unsafe.Pointer(&count)), + uintptr(unsafe.Pointer(&pcreds)), + ) + if ret == 0 { + return nil, err + } + defer procCredFree.Call(pcreds) + credsSlice := *(*[]*sysCREDENTIAL)(unsafe.Pointer(&reflect.SliceHeader{ + Data: pcreds, + Len: count, + Cap: count, + })) + creds := make([]*Credential, count, count) + for i, cred := range credsSlice { + creds[i] = sysToCredential(cred) + } + + return creds, nil +} diff --git a/vendor/github.com/danieljoos/wincred/sys_unsupported.go b/vendor/github.com/danieljoos/wincred/sys_unsupported.go new file mode 100644 index 0000000000..b47bccf8a0 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/sys_unsupported.go @@ -0,0 +1,36 @@ +// +build !windows + +package wincred + +import ( + "errors" + "syscall" +) + +const ( + sysCRED_TYPE_GENERIC = 0 + sysCRED_TYPE_DOMAIN_PASSWORD = 0 + sysCRED_TYPE_DOMAIN_CERTIFICATE = 0 + sysCRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 0 + sysCRED_TYPE_GENERIC_CERTIFICATE = 0 + sysCRED_TYPE_DOMAIN_EXTENDED = 0 + + sysERROR_NOT_FOUND = syscall.Errno(1) + sysERROR_INVALID_PARAMETER = syscall.Errno(1) +) + +func sysCredRead(...interface{}) (*Credential, error) { + return nil, errors.New("Operation not supported") +} + +func sysCredWrite(...interface{}) error { + return errors.New("Operation not supported") +} + +func sysCredDelete(...interface{}) error { + return errors.New("Operation not supported") +} + +func sysCredEnumerate(...interface{}) ([]*Credential, error) { + return nil, errors.New("Operation not supported") +} diff --git a/vendor/github.com/danieljoos/wincred/types.go b/vendor/github.com/danieljoos/wincred/types.go new file mode 100644 index 0000000000..28debc9326 --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/types.go @@ -0,0 +1,69 @@ +package wincred + +import ( + "time" +) + +// CredentialPersistence describes one of three persistence modes of a credential. +// A detailed description of the available modes can be found on +// Docs: https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw +type CredentialPersistence uint32 + +const ( + // PersistSession indicates that the credential only persists for the life + // of the current Windows login session. Such a credential is not visible in + // any other logon session, even from the same user. + PersistSession CredentialPersistence = 0x1 + + // PersistLocalMachine indicates that the credential persists for this and + // all subsequent logon sessions on this local machine/computer. It is + // however not visible for logon sessions of this user on a different + // machine. + PersistLocalMachine CredentialPersistence = 0x2 + + // PersistEnterprise indicates that the credential persists for this and all + // subsequent logon sessions for this user. It is also visible for logon + // sessions on different computers. + PersistEnterprise CredentialPersistence = 0x3 +) + +// CredentialAttribute represents an application-specific attribute of a credential. +type CredentialAttribute struct { + Keyword string + Value []byte +} + +// Credential is the basic credential structure. +// A credential is identified by its target name. +// The actual credential secret is available in the CredentialBlob field. +type Credential struct { + TargetName string + Comment string + LastWritten time.Time + CredentialBlob []byte + Attributes []CredentialAttribute + TargetAlias string + UserName string + Persist CredentialPersistence +} + +// GenericCredential holds a credential for generic usage. +// It is typically defined and used by applications that need to manage user +// secrets. +// +// More information about the available kinds of credentials of the Windows +// Credential Management API can be found on Docs: +// https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/kinds-of-credentials +type GenericCredential struct { + Credential +} + +// DomainPassword holds a domain credential that is typically used by the +// operating system for user logon. +// +// More information about the available kinds of credentials of the Windows +// Credential Management API can be found on Docs: +// https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/kinds-of-credentials +type DomainPassword struct { + Credential +} diff --git a/vendor/github.com/danieljoos/wincred/wincred.go b/vendor/github.com/danieljoos/wincred/wincred.go new file mode 100644 index 0000000000..998de7b5bd --- /dev/null +++ b/vendor/github.com/danieljoos/wincred/wincred.go @@ -0,0 +1,111 @@ +// Package wincred provides primitives for accessing the Windows Credentials Management API. +// This includes functions for retrieval, listing and storage of credentials as well as Go structures for convenient access to the credential data. +// +// A more detailed description of Windows Credentials Management can be found on +// Docs: https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/credentials-management +package wincred + +import "errors" + +const ( + // ErrElementNotFound is the error that is returned if a requested element cannot be found. + // This error constant can be used to check if a credential could not be found. + ErrElementNotFound = sysERROR_NOT_FOUND + + // ErrInvalidParameter is the error that is returned for invalid parameters. + // This error constant can be used to check if the given function parameters were invalid. + // For example when trying to create a new generic credential with an empty target name. + ErrInvalidParameter = sysERROR_INVALID_PARAMETER +) + +// GetGenericCredential fetches the generic credential with the given name from Windows credential manager. +// It returns nil and an error if the credential could not be found or an error occurred. +func GetGenericCredential(targetName string) (*GenericCredential, error) { + cred, err := sysCredRead(targetName, sysCRED_TYPE_GENERIC) + if cred != nil { + return &GenericCredential{Credential: *cred}, err + } + return nil, err +} + +// NewGenericCredential creates a new generic credential object with the given name. +// The persist mode of the newly created object is set to a default value that indicates local-machine-wide storage. +// The credential object is NOT yet persisted to the Windows credential vault. +func NewGenericCredential(targetName string) (result *GenericCredential) { + result = new(GenericCredential) + result.TargetName = targetName + result.Persist = PersistLocalMachine + return +} + +// Write persists the generic credential object to Windows credential manager. +func (t *GenericCredential) Write() (err error) { + err = sysCredWrite(&t.Credential, sysCRED_TYPE_GENERIC) + return +} + +// Delete removes the credential object from Windows credential manager. +func (t *GenericCredential) Delete() (err error) { + err = sysCredDelete(&t.Credential, sysCRED_TYPE_GENERIC) + return +} + +// GetDomainPassword fetches the domain-password credential with the given target host name from Windows credential manager. +// It returns nil and an error if the credential could not be found or an error occurred. +func GetDomainPassword(targetName string) (*DomainPassword, error) { + cred, err := sysCredRead(targetName, sysCRED_TYPE_DOMAIN_PASSWORD) + if cred != nil { + return &DomainPassword{Credential: *cred}, err + } + return nil, err +} + +// NewDomainPassword creates a new domain-password credential used for login to the given target host name. +// The persist mode of the newly created object is set to a default value that indicates local-machine-wide storage. +// The credential object is NOT yet persisted to the Windows credential vault. +func NewDomainPassword(targetName string) (result *DomainPassword) { + result = new(DomainPassword) + result.TargetName = targetName + result.Persist = PersistLocalMachine + return +} + +// Write persists the domain-password credential to Windows credential manager. +func (t *DomainPassword) Write() (err error) { + err = sysCredWrite(&t.Credential, sysCRED_TYPE_DOMAIN_PASSWORD) + return +} + +// Delete removes the domain-password credential from Windows credential manager. +func (t *DomainPassword) Delete() (err error) { + err = sysCredDelete(&t.Credential, sysCRED_TYPE_DOMAIN_PASSWORD) + return +} + +// SetPassword sets the CredentialBlob field of a domain password credential to the given string. +func (t *DomainPassword) SetPassword(pw string) { + t.CredentialBlob = utf16ToByte(utf16FromString(pw)) +} + +// List retrieves all credentials of the Credentials store. +func List() ([]*Credential, error) { + creds, err := sysCredEnumerate("", true) + if err != nil && errors.Is(err, ErrElementNotFound) { + // Ignore ERROR_NOT_FOUND and return an empty list instead + creds = []*Credential{} + err = nil + } + return creds, err +} + +// FilteredList retrieves the list of credentials from the Credentials store that match the given filter. +// The filter string defines the prefix followed by an asterisk for the `TargetName` attribute of the credentials. +func FilteredList(filter string) ([]*Credential, error) { + creds, err := sysCredEnumerate(filter, false) + if err != nil && errors.Is(err, ErrElementNotFound) { + // Ignore ERROR_NOT_FOUND and return an empty list instead + creds = []*Credential{} + err = nil + } + return creds, err +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/.gitignore b/vendor/github.com/dvsekhvalnov/jose2go/.gitignore new file mode 100644 index 0000000000..836562412f --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/.gitignore @@ -0,0 +1,23 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test diff --git a/vendor/github.com/dvsekhvalnov/jose2go/LICENSE b/vendor/github.com/dvsekhvalnov/jose2go/LICENSE new file mode 100644 index 0000000000..326fe253d9 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/dvsekhvalnov/jose2go/README.md b/vendor/github.com/dvsekhvalnov/jose2go/README.md new file mode 100644 index 0000000000..bbf0ef7575 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/README.md @@ -0,0 +1,1023 @@ +# Golang (GO) Javascript Object Signing and Encryption (JOSE) and JSON Web Token (JWT) implementation + +[![GoDoc](https://godoc.org/github.com/dvsekhvalnov/jose2go?status.svg)](http://godoc.org/github.com/dvsekhvalnov/jose2go) + +Pure Golang (GO) library for generating, decoding and encrypting [JSON Web Tokens](https://tools.ietf.org/html/rfc7519). Zero dependency, relies only +on standard library. + +Supports full suite of signing, encryption and compression algorithms defined by [JSON Web Algorithms](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-31) as of July 4, 2014 version. + +Extensively unit tested and cross tested (100+ tests) for compatibility with [jose.4.j](https://bitbucket.org/b_c/jose4j/wiki/Home), [Nimbus-JOSE-JWT](https://bitbucket.org/nimbusds/nimbus-jose-jwt/wiki/Home), [json-jwt](https://github.com/nov/json-jwt) and +[jose-jwt](https://github.com/dvsekhvalnov/jose-jwt) libraries. + + +## Status +Used in production. GA ready. Current version is 1.6. + +## Important +v1.6 security tuning options + +v1.5 bug fix release + +v1.4 changes default behavior of inserting `typ=JWT` header if not overriden. As of 1.4 no +extra headers added by library automatically. To mimic pre 1.4 behaviour use: +```Go +token, err := jose.Sign(..., jose.Header("typ", "JWT")) + +//or + +token, err := jose.Encrypt(..., jose.Header("typ", "JWT")) +``` + +v1.3 fixed potential Invalid Curve Attack on NIST curves within ECDH key management. +Upgrade strongly recommended. + +v1.2 breaks `jose.Decode` interface by returning 3 values instead of 2. + +v1.2 deprecates `jose.Compress` method in favor of using configuration options to `jose.Encrypt`, +the method will be removed in next release. + +### Migration to v1.2 +Pre v1.2 decoding: + +```Go +payload,err := jose.Decode(token,sharedKey) +``` + +Should be updated to v1.2: + +```Go +payload, headers, err := jose.Decode(token,sharedKey) +``` + +Pre v1.2 compression: + +```Go +token,err := jose.Compress(payload,jose.DIR,jose.A128GCM,jose.DEF, key) +``` + +Should be update to v1.2: + +```Go +token, err := jose.Encrypt(payload, jose.DIR, jose.A128GCM, key, jose.Zip(jose.DEF)) +``` + +## Supported JWA algorithms + +**Signing** +- HMAC signatures with HS256, HS384 and HS512. +- RSASSA-PKCS1-V1_5 signatures with RS256, RS384 and RS512. +- RSASSA-PSS signatures (probabilistic signature scheme with appendix) with PS256, PS384 and PS512. +- ECDSA signatures with ES256, ES384 and ES512. +- NONE (unprotected) plain text algorithm without integrity protection + +**Encryption** +- RSAES OAEP (using SHA-1 and MGF1 with SHA-1) encryption with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- RSAES OAEP 256 (using SHA-256 and MGF1 with SHA-256) encryption with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- RSAES-PKCS1-V1_5 encryption with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- A128KW, A192KW, A256KW encryption with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- A128GCMKW, A192GCMKW, A256GCMKW encryption with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- ECDH-ES with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW with A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM, A256GCM +- Direct symmetric key encryption with pre-shared key A128CBC-HS256, A192CBC-HS384, A256CBC-HS512, A128GCM, A192GCM and A256GCM + +**Compression** +- DEFLATE compression + +## Installation +### Grab package from github +`go get github.com/dvsekhvalnov/jose2go` or `go get -u github.com/dvsekhvalnov/jose2go` to update to latest version + +### Import package +```Go +import ( + "github.com/dvsekhvalnov/jose2go" +) +``` + +## Usage +#### Creating Plaintext (unprotected) Tokens + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + token,err := jose.Sign(payload,jose.NONE, nil) + + if(err==nil) { + //go use token + fmt.Printf("\nPlaintext = %v\n",token) + } +} +``` + +### Creating signed tokens +#### HS-256, HS-384 and HS-512 +Signing with HS256, HS384, HS512 expecting `[]byte` array key of corresponding length: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + key := []byte{97,48,97,50,97,98,100,56,45,54,49,54,50,45,52,49,99,51,45,56,51,100,54,45,49,99,102,53,53,57,98,52,54,97,102,99} + + token,err := jose.Sign(payload,jose.HS256,key) + + if(err==nil) { + //go use token + fmt.Printf("\nHS256 = %v\n",token) + } +} +``` + +#### RS-256, RS-384 and RS-512, PS-256, PS-384 and PS-512 +Signing with RS256, RS384, RS512, PS256, PS384, PS512 expecting `*rsa.PrivateKey` private key of corresponding length. **jose2go** [provides convenient utils](#dealing-with-keys) to construct `*rsa.PrivateKey` instance from PEM encoded PKCS1 or PKCS8 data: `Rsa.ReadPrivate([]byte)` under `jose2go/keys/rsa` package. + +```Go +package main + +import ( + "fmt" + "io/ioutil" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + keyBytes,err := ioutil.ReadFile("private.key") + + if(err!=nil) { + panic("invalid key file") + } + + privateKey,e:=Rsa.ReadPrivate(keyBytes) + + if(e!=nil) { + panic("invalid key format") + } + + token,err := jose.Sign(payload,jose.RS256, privateKey) + + if(err==nil) { + //go use token + fmt.Printf("\nRS256 = %v\n",token) + } +} +``` + +#### ES-256, ES-384 and ES-512 +ES256, ES384, ES512 ECDSA signatures expecting `*ecdsa.PrivateKey` private elliptic curve key of corresponding length. **jose2go** [provides convenient utils](#dealing-with-keys) to construct `*ecdsa.PrivateKey` instance from PEM encoded PKCS1 or PKCS8 data: `ecc.ReadPrivate([]byte)` or directly from `X,Y,D` parameters: `ecc.NewPrivate(x,y,d []byte)` under `jose2go/keys/ecc` package. + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello":"world"}` + + privateKey:=ecc.NewPrivate([]byte{4, 114, 29, 223, 58, 3, 191, 170, 67, 128, 229, 33, 242, 178, 157, 150, 133, 25, 209, 139, 166, 69, 55, 26, 84, 48, 169, 165, 67, 232, 98, 9}, + []byte{131, 116, 8, 14, 22, 150, 18, 75, 24, 181, 159, 78, 90, 51, 71, 159, 214, 186, 250, 47, 207, 246, 142, 127, 54, 183, 72, 72, 253, 21, 88, 53}, + []byte{ 42, 148, 231, 48, 225, 196, 166, 201, 23, 190, 229, 199, 20, 39, 226, 70, 209, 148, 29, 70, 125, 14, 174, 66, 9, 198, 80, 251, 95, 107, 98, 206 }) + + token,err := jose.Sign(payload, jose.ES256, privateKey) + + if(err==nil) { + //go use token + fmt.Printf("\ntoken = %v\n",token) + } +} +``` + +### Creating encrypted tokens +#### RSA-OAEP-256, RSA-OAEP and RSA1\_5 key management algorithm +RSA-OAEP-256, RSA-OAEP and RSA1_5 key management expecting `*rsa.PublicKey` public key of corresponding length. + +```Go +package main + +import ( + "fmt" + "io/ioutil" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + keyBytes,err := ioutil.ReadFile("public.key") + + if(err!=nil) { + panic("invalid key file") + } + + publicKey,e:=Rsa.ReadPublic(keyBytes) + + if(e!=nil) { + panic("invalid key format") + } + + //OR: + //token,err := jose.Encrypt(payload, jose.RSA1_5, jose.A256GCM, publicKey) + token,err := jose.Encrypt(payload, jose.RSA_OAEP, jose.A256GCM, publicKey) + + if(err==nil) { + //go use token + fmt.Printf("\ntoken = %v\n",token) + } +} +``` + +#### AES Key Wrap key management family of algorithms +AES128KW, AES192KW and AES256KW key management requires `[]byte` array key of corresponding length + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + sharedKey :=[]byte{194,164,235,6,138,248,171,239,24,216,11,22,137,199,215,133} + + token,err := jose.Encrypt(payload,jose.A128KW,jose.A128GCM,sharedKey) + + if(err==nil) { + //go use token + fmt.Printf("\nA128KW A128GCM = %v\n",token) + } +} +``` + +#### AES GCM Key Wrap key management family of algorithms +AES128GCMKW, AES192GCMKW and AES256GCMKW key management requires `[]byte` array key of corresponding length + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + sharedKey :=[]byte{194,164,235,6,138,248,171,239,24,216,11,22,137,199,215,133} + + token,err := jose.Encrypt(payload,jose.A128GCMKW,jose.A128GCM,sharedKey) + + if(err==nil) { + //go use token + fmt.Printf("\nA128GCMKW A128GCM = %v\n",token) + } +} +``` + +#### ECDH-ES and ECDH-ES with AES Key Wrap key management family of algorithms +ECDH-ES and ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW key management requires `*ecdsa.PublicKey` elliptic curve key of corresponding length. **jose2go** [provides convenient utils](#dealing-with-keys) to construct `*ecdsa.PublicKey` instance from PEM encoded PKCS1 X509 certificate or PKIX data: `ecc.ReadPublic([]byte)` or directly from `X,Y` parameters: `ecc.NewPublic(x,y []byte)`under `jose2go/keys/ecc` package: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello":"world"}` + + publicKey:=ecc.NewPublic([]byte{4, 114, 29, 223, 58, 3, 191, 170, 67, 128, 229, 33, 242, 178, 157, 150, 133, 25, 209, 139, 166, 69, 55, 26, 84, 48, 169, 165, 67, 232, 98, 9}, + []byte{131, 116, 8, 14, 22, 150, 18, 75, 24, 181, 159, 78, 90, 51, 71, 159, 214, 186, 250, 47, 207, 246, 142, 127, 54, 183, 72, 72, 253, 21, 88, 53}) + + token,err := jose.Encrypt(payload, jose.ECDH_ES, jose.A128CBC_HS256, publicKey) + + if(err==nil) { + //go use token + fmt.Printf("\ntoken = %v\n",token) + } +} +``` + +#### PBES2 using HMAC SHA with AES Key Wrap key management family of algorithms +PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW key management requires `string` passphrase from which actual key will be derived + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + passphrase := `top secret` + + token,err := jose.Encrypt(payload,jose.PBES2_HS256_A128KW,jose.A256GCM,passphrase) + + if(err==nil) { + //go use token + fmt.Printf("\nPBES2_HS256_A128KW A256GCM = %v\n",token) + } +} +``` + +#### DIR direct pre-shared symmetric key management +Direct key management with pre-shared symmetric keys expecting `[]byte` array key of corresponding length: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + sharedKey :=[]byte{194,164,235,6,138,248,171,239,24,216,11,22,137,199,215,133} + + token,err := jose.Encrypt(payload,jose.DIR,jose.A128GCM,sharedKey) + + if(err==nil) { + //go use token + fmt.Printf("\nDIR A128GCM = %v\n",token) + } +} +``` + +### Creating compressed & encrypted tokens +#### DEFLATE compression +**jose2go** supports optional DEFLATE compression of payload before encrypting, can be used with all supported encryption and key management algorithms: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := `{"hello": "world"}` + + sharedKey := []byte{194, 164, 235, 6, 138, 248, 171, 239, 24, 216, 11, 22, 137, 199, 215, 133} + + token, err := jose.Encrypt(payload, jose.DIR, jose.A128GCM, sharedKey, jose.Zip(jose.DEF)) + + if err == nil { + //go use token + fmt.Printf("\nDIR A128GCM DEFLATED= %v\n", token) + } +} +``` + +### Verifying, Decoding and Decompressing tokens +Decoding json web tokens is fully symmetric to creating signed or encrypted tokens (with respect to public/private cryptography), decompressing deflated payloads is handled automatically: + +As of v1.2 decode method defined as `jose.Decode() payload string, headers map[string]interface{}, err error` and returns both payload as unprocessed string and headers as map. + +**HS256, HS384, HS512** signatures, **A128KW, A192KW, A256KW**,**A128GCMKW, A192GCMKW, A256GCMKW** and **DIR** key management algorithm expecting `[]byte` array key: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := "eyJhbGciOiJIUzI1NiIsImN0eSI6InRleHRcL3BsYWluIn0.eyJoZWxsbyI6ICJ3b3JsZCJ9.chIoYWrQMA8XL5nFz6oLDJyvgHk2KA4BrFGrKymjC8E" + + sharedKey :=[]byte{97,48,97,50,97,98,100,56,45,54,49,54,50,45,52,49,99,51,45,56,51,100,54,45,49,99,102,53,53,57,98,52,54,97,102,99} + + payload, headers, err := jose.Decode(token,sharedKey) + + if(err==nil) { + //go use token + fmt.Printf("\npayload = %v\n",payload) + + //and/or use headers + fmt.Printf("\nheaders = %v\n",headers) + } +} +``` + +**RS256, RS384, RS512**,**PS256, PS384, PS512** signatures expecting `*rsa.PublicKey` public key of corresponding length. **jose2go** [provides convenient utils](#dealing-with-keys) to construct `*rsa.PublicKey` instance from PEM encoded PKCS1 X509 certificate or PKIX data: `Rsa.ReadPublic([]byte)` under `jose2go/keys/rsa` package: + +```Go +package main + +import ( + "fmt" + "io/ioutil" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := "eyJhbGciOiJSUzI1NiIsImN0eSI6InRleHRcL3BsYWluIn0.eyJoZWxsbyI6ICJ3b3JsZCJ9.NL_dfVpZkhNn4bZpCyMq5TmnXbT4yiyecuB6Kax_lV8Yq2dG8wLfea-T4UKnrjLOwxlbwLwuKzffWcnWv3LVAWfeBxhGTa0c4_0TX_wzLnsgLuU6s9M2GBkAIuSMHY6UTFumJlEeRBeiqZNrlqvmAzQ9ppJHfWWkW4stcgLCLMAZbTqvRSppC1SMxnvPXnZSWn_Fk_q3oGKWw6Nf0-j-aOhK0S0Lcr0PV69ZE4xBYM9PUS1MpMe2zF5J3Tqlc1VBcJ94fjDj1F7y8twmMT3H1PI9RozO-21R0SiXZ_a93fxhE_l_dj5drgOek7jUN9uBDjkXUwJPAyp9YPehrjyLdw" + + keyBytes, err := ioutil.ReadFile("public.key") + + if(err!=nil) { + panic("invalid key file") + } + + publicKey, e:=Rsa.ReadPublic(keyBytes) + + if(e!=nil) { + panic("invalid key format") + } + + payload, headers, err := jose.Decode(token, publicKey) + + if(err==nil) { + //go use token + fmt.Printf("\npayload = %v\n",payload) + + //and/or use headers + fmt.Printf("\nheaders = %v\n",headers) + } +} +``` + +**RSA-OAEP-256**, **RSA-OAEP** and **RSA1_5** key management algorithms expecting `*rsa.PrivateKey` private key of corresponding length: + +```Go +package main + +import ( + "fmt" + "io/ioutil" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := "eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMjU2R0NNIn0.ixD3WVOkvaxeLKi0kyVqTzM6W2EW25SHHYCAr9473Xq528xSK0AVux6kUtv7QMkQKgkMvO8X4VdvonyGkDZTK2jgYUiI06dz7I1sjWJIbyNVrANbBsmBiwikwB-9DLEaKuM85Lwu6gnzbOF6B9R0428ckxmITCPDrzMaXwYZHh46FiSg9djChUTex0pHGhNDiEIgaINpsmqsOFX1L2Y7KM2ZR7wtpR3kidMV3JlxHdKheiPKnDx_eNcdoE-eogPbRGFdkhEE8Dyass1ZSxt4fP27NwsIer5pc0b922_3XWdi1r1TL_fLvGktHLvt6HK6IruXFHpU4x5Z2gTXWxEIog.zzTNmovBowdX2_hi.QSPSgXn0w25ugvzmu2TnhePn.0I3B9BE064HFNP2E0I7M9g" + + keyBytes, err := ioutil.ReadFile("private.key") + + if(err!=nil) { + panic("invalid key file") + } + + privateKey, e:=Rsa.ReadPrivate(keyBytes) + + if(e!=nil) { + panic("invalid key format") + } + + payload, headers, err := jose.Decode(token, privateKey) + + if(err==nil) { + //go use payload + fmt.Printf("\npayload = %v\n",payload) + + //and/or use headers + fmt.Printf("\nheaders = %v\n",headers) + } +} +``` + +**PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW** key management algorithms expects `string` passpharase as a key + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := `eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJjIjo4MTkyLCJwMnMiOiJlZWpFZTF0YmJVbU5XV2s2In0.J2HTgltxH3p7A2zDgQWpZPgA2CHTSnDmMhlZWeSOMoZ0YvhphCeg-w.FzYG5AOptknu7jsG.L8jAxfxZhDNIqb0T96YWoznQ.yNeOfQWUbm8KuDGZ_5lL_g` + + passphrase := `top secret` + + payload, headers, err := jose.Decode(token,passphrase) + + if(err==nil) { + //go use token + fmt.Printf("\npayload = %v\n",payload) + + //and/or use headers + fmt.Printf("\nheaders = %v\n",headers) + } +} +``` + +**ES256, ES284, ES512** signatures expecting `*ecdsa.PublicKey` public elliptic curve key of corresponding length. **jose2go** [provides convenient utils](#dealing-with-keys) to construct `*ecdsa.PublicKey` instance from PEM encoded PKCS1 X509 certificate or PKIX data: `ecc.ReadPublic([]byte)` or directly from `X,Y` parameters: `ecc.NewPublic(x,y []byte)`under `jose2go/keys/ecc` package: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := "eyJhbGciOiJFUzI1NiIsImN0eSI6InRleHRcL3BsYWluIn0.eyJoZWxsbyI6ICJ3b3JsZCJ9.EVnmDMlz-oi05AQzts-R3aqWvaBlwVZddWkmaaHyMx5Phb2NSLgyI0kccpgjjAyo1S5KCB3LIMPfmxCX_obMKA" + + publicKey:=ecc.NewPublic([]byte{4, 114, 29, 223, 58, 3, 191, 170, 67, 128, 229, 33, 242, 178, 157, 150, 133, 25, 209, 139, 166, 69, 55, 26, 84, 48, 169, 165, 67, 232, 98, 9}, + []byte{131, 116, 8, 14, 22, 150, 18, 75, 24, 181, 159, 78, 90, 51, 71, 159, 214, 186, 250, 47, 207, 246, 142, 127, 54, 183, 72, 72, 253, 21, 88, 53}) + + payload, headers, err := jose.Decode(token, publicKey) + + if(err==nil) { + //go use token + fmt.Printf("\npayload = %v\n",payload) + + //and/or use headers + fmt.Printf("\nheaders = %v\n",headers) + } +} +``` + +**ECDH-ES** and **ECDH-ES+A128KW**, **ECDH-ES+A192KW**, **ECDH-ES+A256KW** key management expecting `*ecdsa.PrivateKey` private elliptic curve key of corresponding length. **jose2go** [provides convenient utils](#dealing-with-keys) to construct `*ecdsa.PrivateKey` instance from PEM encoded PKCS1 or PKCS8 data: `ecc.ReadPrivate([]byte)` or directly from `X,Y,D` parameters: `ecc.NewPrivate(x,y,d []byte)` under `jose2go/keys/ecc` package: + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := "eyJhbGciOiJFQ0RILUVTIiwiZW5jIjoiQTEyOENCQy1IUzI1NiIsImVwayI6eyJrdHkiOiJFQyIsIngiOiItVk1LTG5NeW9IVHRGUlpGNnFXNndkRm5BN21KQkdiNzk4V3FVMFV3QVhZIiwieSI6ImhQQWNReTgzVS01Qjl1U21xbnNXcFZzbHVoZGJSZE1nbnZ0cGdmNVhXTjgiLCJjcnYiOiJQLTI1NiJ9fQ..UA3N2j-TbYKKD361AxlXUA.XxFur_nY1GauVp5W_KO2DEHfof5s7kUwvOgghiNNNmnB4Vxj5j8VRS8vMOb51nYy2wqmBb2gBf1IHDcKZdACkCOMqMIcpBvhyqbuKiZPLHiilwSgVV6ubIV88X0vK0C8ZPe5lEyRudbgFjdlTnf8TmsvuAsdtPn9dXwDjUR23bD2ocp8UGAV0lKqKzpAw528vTfD0gwMG8gt_op8yZAxqqLLljMuZdTnjofAfsW2Rq3Z6GyLUlxR51DAUlQKi6UpsKMJoXTrm1Jw8sXBHpsRqA.UHCYOtnqk4SfhAknCnymaQ" + + privateKey:=ecc.NewPrivate([]byte{4, 114, 29, 223, 58, 3, 191, 170, 67, 128, 229, 33, 242, 178, 157, 150, 133, 25, 209, 139, 166, 69, 55, 26, 84, 48, 169, 165, 67, 232, 98, 9}, + []byte{131, 116, 8, 14, 22, 150, 18, 75, 24, 181, 159, 78, 90, 51, 71, 159, 214, 186, 250, 47, 207, 246, 142, 127, 54, 183, 72, 72, 253, 21, 88, 53}, + []byte{ 42, 148, 231, 48, 225, 196, 166, 201, 23, 190, 229, 199, 20, 39, 226, 70, 209, 148, 29, 70, 125, 14, 174, 66, 9, 198, 80, 251, 95, 107, 98, 206 }) + + payload, headers, err := jose.Decode(token, privateKey) + + if(err==nil) { + //go use token + fmt.Printf("\npayload = %v\n",payload) + + //and/or use headers + fmt.Printf("\nheaders = %v\n",headers) + } +} +``` + +### Adding extra headers +It's possible to pass additional headers while encoding token. **jose2go** provides convenience configuration helpers: `Header(name string, value interface{})` and `Headers(headers map[string]interface{})` that can be passed to `Sign(..)` and `Encrypt(..)` calls. + +Note: **jose2go** do not allow to override `alg`, `enc` and `zip` headers. + +Example of signing with extra headers: +```Go + token, err := jose.Sign(payload, jose.ES256, key, + jose.Header("keyid", "111-222-333"), + jose.Header("trans-id", "aaa-bbb")) +``` + +Encryption with extra headers: +```Go +token, err := jose.Encrypt(payload, jose.DIR, jose.A128GCM, sharedKey, + jose.Headers(map[string]interface{}{"keyid": "111-22-33", "cty": "text/plain"})) +``` + +### Two phase validation +In some cases validation (decoding) key can be unknown prior to examining token content. For instance one can use different keys per token issuer or rely on headers information to determine which key to use, do logging or other things. + +**jose2go** allows to pass `func(headers map[string]interface{}, payload string) key interface{}` callback instead of key to `jose.Decode(..)`. Callback will be executed prior to decoding and integrity validation and will recieve parsed headers and payload as is (for encrypted tokens it will be cipher text). Callback should return key to be used for actual decoding process or `error` if decoding should be stopped, given error object will be returned from `jose.Decode(..)` call. + +Example of decoding token with callback: + +```Go +package main + +import ( + "crypto/rsa" + "fmt" + "github.com/dvsekhvalnov/jose2go" + "github.com/dvsekhvalnov/jose2go/keys/rsa" + "io/ioutil" + "errors" +) + +func main() { + + token := "eyJhbGciOiJSUzI1NiIsImN0eSI6InRleHRcL3BsYWluIn0.eyJoZWxsbyI6ICJ3b3JsZCJ9.NL_dfVpZkhNn4bZpCyMq5TmnXbT4yiyecuB6Kax_lV8Yq2dG8wLfea-T4UKnrjLOwxlbwLwuKzffWcnWv3LVAWfeBxhGTa0c4_0TX_wzLnsgLuU6s9M2GBkAIuSMHY6UTFumJlEeRBeiqZNrlqvmAzQ9ppJHfWWkW4stcgLCLMAZbTqvRSppC1SMxnvPXnZSWn_Fk_q3oGKWw6Nf0-j-aOhK0S0Lcr0PV69ZE4xBYM9PUS1MpMe2zF5J3Tqlc1VBcJ94fjDj1F7y8twmMT3H1PI9RozO-21R0SiXZ_a93fxhE_l_dj5drgOek7jUN9uBDjkXUwJPAyp9YPehrjyLdw" + + payload, _, err := jose.Decode(token, + func(headers map[string]interface{}, payload string) interface{} { + //log something + fmt.Printf("\nHeaders before decoding: %v\n", headers) + fmt.Printf("\nPayload before decoding: %v\n", payload) + + //lookup key based on keyid header as en example + //or lookup based on something from payload, e.g. 'iss' claim for instance + key := FindKey(headers['keyid']) + + if(key==nil) { + return errors.New("Key not found") + } + + return key; + }) + + if err == nil { + //go use token + fmt.Printf("\ndecoded payload = %v\n", payload) + } +} +``` + +Two phase validation can be used for implementing additional things like strict `alg` or `enc` validation, see [Customizing library for security](#customizing-library-for-security) for more information. + +### Working with binary payload +In addition to work with string payloads (typical use-case) `jose2go` supports +encoding and decoding of raw binary data. `jose.DecodeBytes`, `jose.SignBytes` +and `jose.EncryptBytes` functions provides similar interface but accepting +`[]byte` payloads. + +Examples: + +```Go +package main + +import ( + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + token := `eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMjU2R0NNIiwicDJjIjo4MTkyLCJwMnMiOiJlZWpFZTF0YmJVbU5XV2s2In0.J2HTgltxH3p7A2zDgQWpZPgA2CHTSnDmMhlZWeSOMoZ0YvhphCeg-w.FzYG5AOptknu7jsG.L8jAxfxZhDNIqb0T96YWoznQ.yNeOfQWUbm8KuDGZ_5lL_g` + + passphrase := `top secret` + + payload, headers, err := jose.DecodeBytes(token,passphrase) + + if(err==nil) { + //go use token + //payload = []byte{....} + } +} +``` + +```Go +package main + +import ( + "fmt" + "io/ioutil" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := []byte {0x01, 0x02, 0x03, 0x04} + + keyBytes,err := ioutil.ReadFile("private.key") + + if(err!=nil) { + panic("invalid key file") + } + + privateKey,e:=Rsa.ReadPrivate(keyBytes) + + if(e!=nil) { + panic("invalid key format") + } + + token,err := jose.SignBytes(payload,jose.RS256, privateKey) + + if(err==nil) { + //go use token + fmt.Printf("\nRS256 = %v\n",token) + } +} +``` + +```Go +package main + +import ( + "fmt" + "io/ioutil" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "github.com/dvsekhvalnov/jose2go" +) + +func main() { + + payload := []byte {0x01, 0x02, 0x03, 0x04} + + keyBytes,err := ioutil.ReadFile("public.key") + + if(err!=nil) { + panic("invalid key file") + } + + publicKey,e:=Rsa.ReadPublic(keyBytes) + + if(e!=nil) { + panic("invalid key format") + } + + token,err := jose.EncryptBytes(payload, jose.RSA_OAEP, jose.A256GCM, publicKey) + + if(err==nil) { + //go use token + fmt.Printf("\ntoken = %v\n",token) + } +} +``` +### Dealing with keys +**jose2go** provides several helper methods to simplify loading & importing of elliptic and rsa keys. Import `jose2go/keys/rsa` or `jose2go/keys/ecc` respectively: + +#### RSA keys +1. `Rsa.ReadPrivate(raw []byte) (key *rsa.PrivateKey,err error)` attempts to parse RSA private key from PKCS1 or PKCS8 format (`BEGIN RSA PRIVATE KEY` and `BEGIN PRIVATE KEY` headers) + +```Go +package main + +import ( + "fmt" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "io/ioutil" +) + +func main() { + + keyBytes, _ := ioutil.ReadFile("private.key") + + privateKey, err:=Rsa.ReadPrivate(keyBytes) + + if(err!=nil) { + panic("invalid key format") + } + + fmt.Printf("privateKey = %v\n",privateKey) +} +``` + +2. `Rsa.ReadPublic(raw []byte) (key *rsa.PublicKey,err error)` attempts to parse RSA public key from PKIX key format or PKCS1 X509 certificate (`BEGIN PUBLIC KEY` and `BEGIN CERTIFICATE` headers) + +```Go +package main + +import ( + "fmt" + Rsa "github.com/dvsekhvalnov/jose2go/keys/rsa" + "io/ioutil" +) + +func main() { + + keyBytes, _ := ioutil.ReadFile("public.cer") + + publicKey, err:=Rsa.ReadPublic(keyBytes) + + if(err!=nil) { + panic("invalid key format") + } + + fmt.Printf("publicKey = %v\n",publicKey) +} +``` + +#### ECC keys +1. `ecc.ReadPrivate(raw []byte) (key *ecdsa.PrivateKey,err error)` attemps to parse elliptic curve private key from PKCS1 or PKCS8 format (`BEGIN EC PRIVATE KEY` and `BEGIN PRIVATE KEY` headers) + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "io/ioutil" +) + +func main() { + + keyBytes, _ := ioutil.ReadFile("ec-private.pem") + + ecPrivKey, err:=ecc.ReadPrivate(keyBytes) + + if(err!=nil) { + panic("invalid key format") + } + + fmt.Printf("ecPrivKey = %v\n",ecPrivKey) +} +``` + +2. `ecc.ReadPublic(raw []byte) (key *ecdsa.PublicKey,err error)` attemps to parse elliptic curve public key from PKCS1 X509 or PKIX format (`BEGIN PUBLIC KEY` and `BEGIN CERTIFICATE` headers) + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "io/ioutil" +) + +func main() { + + keyBytes, _ := ioutil.ReadFile("ec-public.key") + + ecPubKey, err:=ecc.ReadPublic(keyBytes) + + if(err!=nil) { + panic("invalid key format") + } + + fmt.Printf("ecPubKey = %v\n",ecPubKey) +} +``` + +3. `ecc.NewPublic(x,y []byte) (*ecdsa.PublicKey)` constructs elliptic public key from (X,Y) represented as bytes. Supported are NIST curves P-256,P-384 and P-521. Curve detected automatically by input length. + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" +) + +func main() { + + ecPubKey:=ecc.NewPublic([]byte{4, 114, 29, 223, 58, 3, 191, 170, 67, 128, 229, 33, 242, 178, 157, 150, 133, 25, 209, 139, 166, 69, 55, 26, 84, 48, 169, 165, 67, 232, 98, 9}, + []byte{131, 116, 8, 14, 22, 150, 18, 75, 24, 181, 159, 78, 90, 51, 71, 159, 214, 186, 250, 47, 207, 246, 142, 127, 54, 183, 72, 72, 253, 21, 88, 53}) + + fmt.Printf("ecPubKey = %v\n",ecPubKey) +} +``` + +4. `ecc.NewPrivate(x,y,d []byte) (*ecdsa.PrivateKey)` constructs elliptic private key from (X,Y) and D represented as bytes. Supported are NIST curves P-256,P-384 and P-521. Curve detected automatically by input length. + +```Go +package main + +import ( + "fmt" + "github.com/dvsekhvalnov/jose2go/keys/ecc" +) + +func main() { + + ecPrivKey:=ecc.NewPrivate([]byte{4, 114, 29, 223, 58, 3, 191, 170, 67, 128, 229, 33, 242, 178, 157, 150, 133, 25, 209, 139, 166, 69, 55, 26, 84, 48, 169, 165, 67, 232, 98, 9}, + []byte{131, 116, 8, 14, 22, 150, 18, 75, 24, 181, 159, 78, 90, 51, 71, 159, 214, 186, 250, 47, 207, 246, 142, 127, 54, 183, 72, 72, 253, 21, 88, 53}, + []byte{ 42, 148, 231, 48, 225, 196, 166, 201, 23, 190, 229, 199, 20, 39, 226, 70, 209, 148, 29, 70, 125, 14, 174, 66, 9, 198, 80, 251, 95, 107, 98, 206 }) + + fmt.Printf("ecPrivKey = %v\n",ecPrivKey) +} +``` + +### More examples +Checkout `jose_test.go` for more examples. + +## Customizing library for security +In response to ever increasing attacks on various JWT implementations, `jose2go` as of version v1.6 introduced number of additional security controls to limit potential attack surface on services and projects using the library. + +### Deregister algorithm implementations +One can use following methods to deregister any signing, encryption, key management or compression algorithms from runtime suite, that is considered unsafe or simply not expected by service. + +- `func DeregisterJwa(alg string) JwaAlgorithm` +- `func DeregisterJwe(alg string) JweEncryption` +- `func DeregisterJws(alg string) JwsAlgorithm` +- `func DeregisterJwc(alg string) JwcAlgorithm` + +All of them expecting alg name matching `jose` constants and returns implementation that have been deregistered. + +### Strict validation +Sometimes it is desirable to verify that `alg` or `enc` values are matching expected before attempting to decode actual payload. +`jose2go` provides helper matchers to be used within [Two-phase validation](#two-phase-validation) precheck: + +- `jose.Alg(key, alg)` - to match alg header +- `jose.Enc(key, alg)` - to match alg and enc headers + +```Go + token := "eyJhbGciOiJSUzI1NiIsImN0eSI6InRleHRcL3BsYWluIn0.eyJoZWxsbyI6ICJ3b3JsZCJ9.NL_dfVpZkhNn4bZpCyMq5TmnXbT4yiyecuB6Kax_lV8Yq2dG8wLfea-T4UKnrjLOwxlbwLwuKzffWcnWv3LVAWfeBxhGTa0c4_0TX_wzLnsgLuU6s9M2GBkAIuSMHY6UTFumJlEeRBeiqZNrlqvmAzQ9ppJHfWWkW4stcgLCLMAZbTqvRSppC1SMxnvPXnZSWn_Fk_q3oGKWw6Nf0-j-aOhK0S0Lcr0PV69ZE4xBYM9PUS1MpMe2zF5J3Tqlc1VBcJ94fjDj1F7y8twmMT3H1PI9RozO-21R0SiXZ_a93fxhE_l_dj5drgOek7jUN9uBDjkXUwJPAyp9YPehrjyLdw" + + key := Rsa.ReadPublic(....) + + // we expecting 'RS256' alg here and if matching continue to decode with a key + payload, header, err := jose.Decode(token, Alg(key, "RS256")) + + // or match both alg and enc for decrypting scenarios + payload, header, err := jose.Decode(token, Enc(key, "RSA-OAEP-256", "A192CBC-HS384")) +``` + +### Customizing PBKDF2 +As it quite easy to abuse PBES2 family of algorithms via forging header with extra large p2c values, jose-jwt library introduced iteration count limits in v1.6 to reduce runtime exposure. + +By default, maxIterations is set according to [OWASP PBKDF2](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) Recomendations: + +``` +PBES2-HS256+A128KW: 1300000 +PBES2-HS384+A192KW: 950000 +PBES2-HS512+A256KW: 600000 +``` + +, while minIterations kept at 0 for backward compatibility. + +If it is desired to implement different limits, register new implementation with new parameters: + +```Go + jose.RegisterJwa(NewPbse2HmacAesKWAlg(128, 1300000, 1300000)) + jose.RegisterJwa(NewPbse2HmacAesKWAlg(192, 950000, 950000)) + jose.RegisterJwa(NewPbse2HmacAesKWAlg(256, 600000, 600000)) +``` + +In case you can't upgrade to latest version, but would like to have protections against PBES2 abuse, it is recommended to stick with [Two-phase validation](#two-phase-validation) precheck before decoding: + +```Go +test, headers, err := Decode(token, func(headers map[string]interface{}, payload string) interface{} { + alg := headers["alg"].(string) + p2c := headers["p2c"].(float64) + + if strings.HasPrefix(alg, "PBES2-") && int64(p2c) > 100 { + return errors.New("Too many p2c interation count, aborting") + } + + return "top secret" +}) +``` + +## Changelog +### 1.6 +- ability to deregister specific algorithms +- configurable min/max restrictions for PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW + +### 1.5 +- security and bug fixes + +### 1.4 +- removed extra headers to be inserted by library + +### 1.3 +- security fixes: Invalid Curve Attack on NIST curves + +### 1.2 +- interface to access token headers after decoding +- interface to provide extra headers for token encoding +- two-phase validation support + +### 1.1 +- security and bug fixes + +### 1.0 +- initial stable version with full suite JOSE spec support diff --git a/vendor/github.com/dvsekhvalnov/jose2go/aes/ecb.go b/vendor/github.com/dvsekhvalnov/jose2go/aes/ecb.go new file mode 100644 index 0000000000..ec9af99dce --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/aes/ecb.go @@ -0,0 +1,68 @@ +// Package aes contains provides AES Key Wrap and ECB mode implementations +package aes + +import ( + "crypto/cipher" +) + +type ecb struct { + b cipher.Block +} + +type ecbEncrypter ecb +type ecbDecrypter ecb + +// NewECBEncrypter creates BlockMode for AES encryption in ECB mode +func NewECBEncrypter(b cipher.Block) cipher.BlockMode { + return &ecbEncrypter{b: b} +} + +// NewECBDecrypter creates BlockMode for AES decryption in ECB mode +func NewECBDecrypter(b cipher.Block) cipher.BlockMode { + return &ecbDecrypter{b: b} +} + +func (x *ecbEncrypter) BlockSize() int { return x.b.BlockSize() } +func (x *ecbDecrypter) BlockSize() int { return x.b.BlockSize() } + +func (x *ecbDecrypter) CryptBlocks(dst, src []byte) { + bs := x.BlockSize() + + if len(src)%bs != 0 { + panic("ecbDecrypter.CryptBlocks(): input not full blocks") + } + + if len(dst) < len(src) { + panic("ecbDecrypter.CryptBlocks(): output smaller than input") + } + + if len(src) == 0 { + return + } + + for len(src) > 0 { + x.b.Decrypt(dst, src) + src = src[bs:] + } +} + +func (x *ecbEncrypter) CryptBlocks(dst, src []byte) { + bs := x.BlockSize() + + if len(src)%bs != 0 { + panic("ecbEncrypter.CryptBlocks(): input not full blocks") + } + + if len(dst) < len(src) { + panic("ecbEncrypter.CryptBlocks(): output smaller than input") + } + + if len(src) == 0 { + return + } + + for len(src) > 0 { + x.b.Encrypt(dst, src) + src = src[bs:] + } +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/aes/key_wrap.go b/vendor/github.com/dvsekhvalnov/jose2go/aes/key_wrap.go new file mode 100644 index 0000000000..8e5963eaf0 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/aes/key_wrap.go @@ -0,0 +1,113 @@ +package aes + +import ( + "github.com/dvsekhvalnov/jose2go/arrays" + "crypto/cipher" + "crypto/aes" + "crypto/hmac" + "errors" +) + +var defaultIV=[]byte { 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6 } + +// KeyWrap encrypts provided key (CEK) with KEK key using AES Key Wrap (rfc 3394) algorithm +func KeyWrap(cek,kek []byte) ([]byte,error) { + // 1) Initialize variables + a := defaultIV // Set A = IV, an initial value + r := arrays.Slice(cek, 8) // For i = 1 to n + // R[0][i] = P[i] + n := uint64(len(r)) + + // 2) Calculate intermediate values. + var j,i,t uint64 + + for j = 0; j < 6; j++ { // For j = 0 to 5 + for i = 0; i < n; i++ { // For i=1 to n + t = n * j + i + 1; + b,e := aesEnc(kek, arrays.Concat(a, r[i])) // B=AES(K, A | R[i]) + + if e!=nil { return nil, e } + + a = b[:len(b)/2] // A=MSB(64,B) ^ t where t = (n*j)+i + r[i] = b[len(b)/2:] // R[i] = LSB(64, B) + a = arrays.Xor(a, arrays.UInt64ToBytes(t)) + } + } + + // 3) Output the results + c := make([][]byte, n+1, n+1) + c[0] = a; // Set C[0] = A + for i = 1; i <= n; i++ { // For i = 1 to n + c[i] = r[i - 1] // C[i] = R[i] + } + + return arrays.Unwrap(c),nil +} + +// KeyUnwrap decrypts previously encrypted key (CEK) with KEK key using AES Key Wrap (rfc 3394) algorithm +func KeyUnwrap(encryptedCek, kek []byte) ([]byte,error) { + // 1) Initialize variables + c := arrays.Slice(encryptedCek, 8); + a := c[0]; // Set A = C[0] + r := make([][]byte,len(c) - 1); + + for i := 1; i < len(c); i++ { // For i = 1 to n + r[i - 1] = c[i]; // R[i] = C[i] + } + + n := uint64(len(r)) + + // 2) Calculate intermediate values + var t,j uint64 + + for j = 6; j > 0; j-- { // For j = 5 to 0 + for i := n; i > 0; i-- { // For i = n to 1 + t = n * (j-1) + i; + a = arrays.Xor(a, arrays.UInt64ToBytes(t)) + b,e := aesDec(kek, arrays.Concat(a, r[i-1])) // B = AES-1(K, (A ^ t) | R[i]) where t = n*j+i + + if e!=nil { return nil,e } + + a = b[:len(b)/2] // A = MSB(64, B) + r[i-1] = b[len(b)/2:] // R[i] = LSB(64, B) + } + } + + // 3) Output the results + if (!hmac.Equal(defaultIV, a)) { // If A is an appropriate initial value + return nil, errors.New("aes.KeyUnwrap(): integrity check failed.") + } + + // For i = 1 to n + return arrays.Unwrap(r),nil // P[i] = R[i] + +} + +func aesEnc(kek, plainText []byte) (cipherText []byte, err error) { + var block cipher.Block + + if block, err = aes.NewCipher(kek);err!=nil { + return nil,err + } + + cipherText = make([]byte, len(plainText)) + + NewECBEncrypter(block).CryptBlocks(cipherText,plainText) + + return cipherText,nil +} + +func aesDec(kek, cipherText []byte) (plainText []byte,err error) { + + var block cipher.Block + + if block, err = aes.NewCipher(kek);err!=nil { + return nil,err + } + + plainText = make([]byte, len(cipherText)) + + NewECBDecrypter(block).CryptBlocks(plainText,cipherText) + + return plainText,nil +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/aes_cbc_hmac.go b/vendor/github.com/dvsekhvalnov/jose2go/aes_cbc_hmac.go new file mode 100644 index 0000000000..a2217e17e0 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/aes_cbc_hmac.go @@ -0,0 +1,112 @@ +package jose + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "errors" + "fmt" + "github.com/dvsekhvalnov/jose2go/arrays" + "github.com/dvsekhvalnov/jose2go/padding" +) + +// AES CBC with HMAC authenticated encryption algorithm implementation +type AesCbcHmac struct { + keySizeBits int +} + +func init() { + RegisterJwe(&AesCbcHmac{keySizeBits: 256}) + RegisterJwe(&AesCbcHmac{keySizeBits: 384}) + RegisterJwe(&AesCbcHmac{keySizeBits: 512}) +} + +func (alg *AesCbcHmac) Name() string { + switch alg.keySizeBits { + case 256: + return A128CBC_HS256 + case 384: + return A192CBC_HS384 + default: + return A256CBC_HS512 + } +} + +func (alg *AesCbcHmac) KeySizeBits() int { + return alg.keySizeBits +} + +func (alg *AesCbcHmac) SetKeySizeBits(bits int) { + alg.keySizeBits = bits +} + +func (alg *AesCbcHmac) Encrypt(aad, plainText, cek []byte) (iv, cipherText, authTag []byte, err error) { + + cekSizeBits := len(cek) << 3 + if cekSizeBits != alg.keySizeBits { + return nil, nil, nil, errors.New(fmt.Sprintf("AesCbcHmac.Encrypt(): expected key of size %v bits, but was given %v bits.", alg.keySizeBits, cekSizeBits)) + } + + hmacKey := cek[0 : len(cek)/2] + aesKey := cek[len(cek)/2:] + + if iv, err = arrays.Random(16); err != nil { + return nil, nil, nil, err + } + + var block cipher.Block + + if block, err = aes.NewCipher(aesKey); err != nil { + return nil, nil, nil, err + } + + padded := padding.AddPkcs7(plainText, 16) + + cipherText = make([]byte, len(padded), cap(padded)) + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(cipherText, padded) + + authTag = alg.computeAuthTag(aad, iv, cipherText, hmacKey) + + return iv, cipherText, authTag, nil +} + +func (alg *AesCbcHmac) Decrypt(aad, cek, iv, cipherText, authTag []byte) (plainText []byte, err error) { + + cekSizeBits := len(cek) << 3 + + if cekSizeBits != alg.keySizeBits { + return nil, errors.New(fmt.Sprintf("AesCbcHmac.Decrypt(): expected key of size %v bits, but was given %v bits.", alg.keySizeBits, cekSizeBits)) + } + + hmacKey := cek[0 : len(cek)/2] + aesKey := cek[len(cek)/2:] + + // Check MAC + expectedAuthTag := alg.computeAuthTag(aad, iv, cipherText, hmacKey) + + if !hmac.Equal(expectedAuthTag, authTag) { + return nil, errors.New("AesCbcHmac.Decrypt(): Authentication tag do not match.") + } + + var block cipher.Block + + if block, err = aes.NewCipher(aesKey); err == nil { + mode := cipher.NewCBCDecrypter(block, iv) + + var padded []byte = make([]byte, len(cipherText), cap(cipherText)) + mode.CryptBlocks(padded, cipherText) + + return padding.RemovePkcs7(padded, 16), nil + } + + return nil, err +} + +func (alg *AesCbcHmac) computeAuthTag(aad []byte, iv []byte, cipherText []byte, hmacKey []byte) (signature []byte) { + al := arrays.UInt64ToBytes(uint64(len(aad) << 3)) + hmacInput := arrays.Concat(aad, iv, cipherText, al) + hmac := calculateHmac(alg.keySizeBits, hmacInput, hmacKey) + + return hmac[0 : len(hmac)/2] +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/aes_gcm.go b/vendor/github.com/dvsekhvalnov/jose2go/aes_gcm.go new file mode 100644 index 0000000000..57fc861550 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/aes_gcm.go @@ -0,0 +1,98 @@ +package jose + +import ( + "fmt" + "errors" + "crypto/aes" + "crypto/cipher" + "github.com/dvsekhvalnov/jose2go/arrays" +) + +// AES GCM authenticated encryption algorithm implementation +type AesGcm struct{ + keySizeBits int +} + +func init() { + RegisterJwe(&AesGcm{keySizeBits:128}) + RegisterJwe(&AesGcm{keySizeBits:192}) + RegisterJwe(&AesGcm{keySizeBits:256}) +} + +func (alg *AesGcm) Name() string { + switch alg.keySizeBits { + case 128: return A128GCM + case 192: return A192GCM + default: return A256GCM + } +} + +func (alg *AesGcm) KeySizeBits() int { + return alg.keySizeBits +} + +func (alg *AesGcm) Encrypt(aad, plainText, cek []byte) (iv, cipherText, authTag []byte, err error) { + + cekSizeBits := len(cek)<<3 + + if cekSizeBits != alg.keySizeBits { + return nil,nil,nil, errors.New(fmt.Sprintf("AesGcm.Encrypt(): expected key of size %v bits, but was given %v bits.",alg.keySizeBits, cekSizeBits)) + } + + if iv,err = arrays.Random(12);err!=nil { + return nil,nil,nil,err + } + + var block cipher.Block + + if block, err = aes.NewCipher(cek);err!=nil { + return nil,nil,nil,err + } + + var aesgcm cipher.AEAD + + if aesgcm,err = cipher.NewGCM(block);err!=nil { + return nil,nil,nil,err + } + + cipherWithTag := aesgcm.Seal(nil, iv, plainText, aad) + + cipherText=cipherWithTag[:len(cipherWithTag)-aesgcm.Overhead()] + authTag=cipherWithTag[len(cipherWithTag)-aesgcm.Overhead():] + + return iv, cipherText, authTag, nil +} + +func (alg *AesGcm) Decrypt(aad, cek, iv, cipherText, authTag []byte) (plainText []byte, err error) { + + cekSizeBits := len(cek)<<3 + + if cekSizeBits != alg.keySizeBits { + return nil, errors.New(fmt.Sprintf("AesGcm.Decrypt(): expected key of size %v bits, but was given %v bits.",alg.keySizeBits, cekSizeBits)) + } + + var block cipher.Block + + if block, err = aes.NewCipher(cek);err!=nil { + return nil,err + } + + var aesgcm cipher.AEAD + + if aesgcm,err = cipher.NewGCM(block);err!=nil { + return nil,err + } + + cipherWithTag:=append(cipherText,authTag...) + + if nonceSize := len(iv); nonceSize != aesgcm.NonceSize() { + return nil, errors.New(fmt.Sprintf("AesGcm.Decrypt(): expected nonce of size %v bits, but was given %v bits.", aesgcm.NonceSize()<<3, nonceSize<<3)) + } + + if plainText,err = aesgcm.Open(nil, iv, cipherWithTag, aad);err!=nil { + return nil,err + } + + return plainText,nil +} + diff --git a/vendor/github.com/dvsekhvalnov/jose2go/aes_gcm_kw.go b/vendor/github.com/dvsekhvalnov/jose2go/aes_gcm_kw.go new file mode 100644 index 0000000000..e6f568180a --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/aes_gcm_kw.go @@ -0,0 +1,128 @@ +package jose + +import ( + "errors" + "fmt" + "github.com/dvsekhvalnov/jose2go/base64url" + "github.com/dvsekhvalnov/jose2go/arrays" + "crypto/aes" + "crypto/cipher" +) + +func init() { + RegisterJwa(&AesGcmKW{ keySizeBits: 128}) + RegisterJwa(&AesGcmKW{ keySizeBits: 192}) + RegisterJwa(&AesGcmKW{ keySizeBits: 256}) +} + +// AES GCM Key Wrap key management algorithm implementation +type AesGcmKW struct { + keySizeBits int +} + +func (alg *AesGcmKW) Name() string { + switch alg.keySizeBits { + case 128: return A128GCMKW + case 192: return A192GCMKW + default: return A256GCMKW + } +} + +func (alg *AesGcmKW) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + if kek,ok:=key.([]byte); ok { + + kekSizeBits := len(kek) << 3 + + if kekSizeBits != alg.keySizeBits { + return nil,nil, errors.New(fmt.Sprintf("AesGcmKW.WrapNewKey(): expected key of size %v bits, but was given %v bits.",alg.keySizeBits, kekSizeBits)) + } + + if cek,err = arrays.Random(cekSizeBits>>3);err!=nil { + return nil,nil,err + } + + var iv []byte + + if iv,err = arrays.Random(12);err!=nil { + return nil,nil,err + } + + var block cipher.Block + + if block, err = aes.NewCipher(kek);err!=nil { + return nil,nil,err + } + + var aesgcm cipher.AEAD + + if aesgcm,err = cipher.NewGCM(block);err!=nil { + return nil,nil,err + } + + cipherWithTag := aesgcm.Seal(nil, iv, cek, nil) + + cipherText := cipherWithTag[:len(cipherWithTag)-aesgcm.Overhead()] + authTag := cipherWithTag[len(cipherWithTag)-aesgcm.Overhead():] + + header["iv"]=base64url.Encode(iv) + header["tag"]=base64url.Encode(authTag) + + return cek,cipherText,nil + } + + return nil,nil,errors.New("AesGcmKW.WrapNewKey(): expected key to be '[]byte' array") +} + +func (alg *AesGcmKW) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + if kek,ok:=key.([]byte); ok { + + kekSizeBits := len(kek) << 3 + + if kekSizeBits != alg.keySizeBits { + return nil,errors.New(fmt.Sprintf("AesGcmKW.Unwrap(): expected key of size %v bits, but was given %v bits.", alg.keySizeBits, kekSizeBits)) + } + + var iv,tag string + + if iv,ok = header["iv"].(string);!ok { + return nil,errors.New("AesGcmKW.Unwrap(): expected 'iv' param in JWT header, but was not found.") + } + + if tag,ok = header["tag"].(string);!ok { + return nil,errors.New("AesGcmKW.Unwrap(): expected 'tag' param in JWT header, but was not found.") + } + + var ivBytes,tagBytes []byte + + if ivBytes,err = base64url.Decode(iv);err!=nil { + return nil,err + } + + if tagBytes,err = base64url.Decode(tag);err!=nil { + return nil,err + } + + var block cipher.Block + + if block, err = aes.NewCipher(kek);err!=nil { + return nil,err + } + + var aesgcm cipher.AEAD + + if aesgcm,err = cipher.NewGCM(block);err!=nil { + return nil,err + } + + cipherAndTag:=append(encryptedCek,tagBytes...) + + if cek,err = aesgcm.Open(nil, ivBytes,cipherAndTag , nil);err!=nil { + fmt.Printf("err = %v\n",err) + return nil,err + } + + return cek,nil + } + + return nil,errors.New("AesGcmKW.Unwrap(): expected key to be '[]byte' array") +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/aeskw.go b/vendor/github.com/dvsekhvalnov/jose2go/aeskw.go new file mode 100644 index 0000000000..c5b3e4f6da --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/aeskw.go @@ -0,0 +1,64 @@ +package jose + +import ( + "errors" + "fmt" + "github.com/dvsekhvalnov/jose2go/aes" + "github.com/dvsekhvalnov/jose2go/arrays" +) + +func init() { + RegisterJwa(&AesKW{ keySizeBits: 128}) + RegisterJwa(&AesKW{ keySizeBits: 192}) + RegisterJwa(&AesKW{ keySizeBits: 256}) +} + +// AES Key Wrap key management algorithm implementation +type AesKW struct { + keySizeBits int +} + +func (alg *AesKW) Name() string { + switch alg.keySizeBits { + case 128: return A128KW + case 192: return A192KW + default: return A256KW + } +} + +func (alg *AesKW) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + if kek,ok:=key.([]byte); ok { + + kekSizeBits := len(kek) << 3 + + if kekSizeBits != alg.keySizeBits { + return nil,nil, errors.New(fmt.Sprintf("AesKW.WrapNewKey(): expected key of size %v bits, but was given %v bits.",alg.keySizeBits, kekSizeBits)) + } + + if cek,err = arrays.Random(cekSizeBits>>3);err==nil { + encryptedCek,err=aes.KeyWrap(cek,kek) + return + } + + return nil,nil,err + + } + + return nil,nil,errors.New("AesKW.WrapNewKey(): expected key to be '[]byte' array") +} + +func (alg *AesKW) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + + if kek,ok:=key.([]byte); ok { + + kekSizeBits := len(kek) << 3 + + if kekSizeBits != alg.keySizeBits { + return nil,errors.New(fmt.Sprintf("AesKW.Unwrap(): expected key of size %v bits, but was given %v bits.", alg.keySizeBits, kekSizeBits)) + } + + return aes.KeyUnwrap(encryptedCek, kek) + } + + return nil,errors.New("AesKW.Unwrap(): expected key to be '[]byte' array") +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/arrays/arrays.go b/vendor/github.com/dvsekhvalnov/jose2go/arrays/arrays.go new file mode 100644 index 0000000000..17ff0bd86e --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/arrays/arrays.go @@ -0,0 +1,116 @@ +// Package arrays provides various byte array utilities +package arrays + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "fmt" + "github.com/dvsekhvalnov/jose2go/base64url" +) + +// Xor is doing byte by byte exclusive or of 2 byte arrays +func Xor(left, right []byte) []byte { + result := make([]byte, len(left)) + + for i := 0; i < len(left); i++ { + result[i] = left[i] ^ right[i] + } + + return result +} + +// Slice is splitting input byte array into slice of subarrays. Each of count length. +func Slice(arr []byte, count int) [][]byte { + + sliceCount := len(arr) / count + result := make([][]byte, sliceCount) + + for i := 0; i < sliceCount; i++ { + start := i * count + end := i*count + count + + result[i] = arr[start:end] + } + + return result +} + +// Random generates byte array with random data of byteCount length +func Random(byteCount int) ([]byte, error) { + data := make([]byte, byteCount) + + if _, err := rand.Read(data); err != nil { + return nil, err + } + + return data, nil +} + +// Concat combine several arrays into single one, resulting slice = A1 | A2 | A3 | ... | An +func Concat(arrays ...[]byte) []byte { + var result []byte = arrays[0] + + for _, arr := range arrays[1:] { + result = append(result, arr...) + } + + return result +} + +// Unwrap same thing as Contact, just different interface, combines several array into single one +func Unwrap(arrays [][]byte) []byte { + var result []byte = arrays[0] + + for _, arr := range arrays[1:] { + result = append(result, arr...) + } + + return result +} + +// UInt64ToBytes unwrap uint64 value to byte array of length 8 using big endian +func UInt64ToBytes(value uint64) []byte { + result := make([]byte, 8) + binary.BigEndian.PutUint64(result, value) + + return result +} + +// UInt32ToBytes unwrap uint32 value to byte array of length 4 using big endian +func UInt32ToBytes(value uint32) []byte { + result := make([]byte, 4) + binary.BigEndian.PutUint32(result, value) + + return result +} + +// Dump produces printable debug representation of byte array as string +func Dump(arr []byte) string { + var buf bytes.Buffer + + buf.WriteString("(") + buf.WriteString(fmt.Sprintf("%v", len(arr))) + buf.WriteString(" bytes)[") + + for idx, b := range arr { + buf.WriteString(fmt.Sprintf("%v", b)) + if idx != len(arr)-1 { + buf.WriteString(", ") + } + } + + buf.WriteString("], Hex: [") + + for idx, b := range arr { + buf.WriteString(fmt.Sprintf("%X", b)) + if idx != len(arr)-1 { + buf.WriteString(" ") + } + } + + buf.WriteString("], Base64Url:") + buf.WriteString(base64url.Encode(arr)) + + return buf.String() +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/base64url/base64url.go b/vendor/github.com/dvsekhvalnov/jose2go/base64url/base64url.go new file mode 100644 index 0000000000..7229a85eb7 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/base64url/base64url.go @@ -0,0 +1,31 @@ +// package base64url provides base64url encoding/decoding support +package base64url + +import ( + "strings" + "encoding/base64" +) + +// Decode decodes base64url string to byte array +func Decode(data string) ([]byte,error) { + data = strings.Replace(data, "-", "+", -1) // 62nd char of encoding + data = strings.Replace(data, "_", "/", -1) // 63rd char of encoding + + switch(len(data) % 4) { // Pad with trailing '='s + case 0: // no padding + case 2: data+="==" // 2 pad chars + case 3: data+="=" // 1 pad char + } + + return base64.StdEncoding.DecodeString(data) +} + +// Encode encodes given byte array to base64url string +func Encode(data []byte) string { + result := base64.StdEncoding.EncodeToString(data) + result = strings.Replace(result, "+", "-", -1) // 62nd char of encoding + result = strings.Replace(result, "/", "_", -1) // 63rd char of encoding + result = strings.Replace(result, "=", "", -1) // Remove any trailing '='s + + return result +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/compact/compact.go b/vendor/github.com/dvsekhvalnov/jose2go/compact/compact.go new file mode 100644 index 0000000000..1e9dd2f51c --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/compact/compact.go @@ -0,0 +1,33 @@ +// package compact provides function to work with json compact serialization format +package compact + +import ( + "github.com/dvsekhvalnov/jose2go/base64url" + "strings" +) + +// Parse splitting & decoding compact serialized json web token, returns slice of byte arrays, each representing part of token +func Parse(token string) (result [][]byte, e error) { + parts := strings.Split(token, ".") + + result = make([][]byte, len(parts)) + + for i, part := range parts { + if result[i], e = base64url.Decode(part); e != nil { + return nil, e + } + } + + return result, nil +} + +// Serialize converts given parts into compact serialization format +func Serialize(parts ...[]byte) string { + result := make([]string, len(parts)) + + for i, part := range parts { + result[i] = base64url.Encode(part) + } + + return strings.Join(result, ".") +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/deflate.go b/vendor/github.com/dvsekhvalnov/jose2go/deflate.go new file mode 100644 index 0000000000..c788f5bdcc --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/deflate.go @@ -0,0 +1,39 @@ +package jose + +import ( + "bytes" + "compress/flate" + "io/ioutil" +) + +func init() { + RegisterJwc(new(Deflate)) +} + +// Deflate compression algorithm implementation +type Deflate struct {} + +func (alg *Deflate) Name() string { + return DEF +} + +func (alg *Deflate) Compress(plainText []byte) []byte { + var buf bytes.Buffer + deflate,_ := flate.NewWriter(&buf, 8) //level=DEFLATED + + deflate.Write(plainText) + deflate.Close() + + return buf.Bytes() +} + +func (alg *Deflate) Decompress(compressedText []byte) []byte { + + enflated,_ := ioutil.ReadAll( + flate.NewReader( + bytes.NewReader(compressedText))) + + return enflated +} + + diff --git a/vendor/github.com/dvsekhvalnov/jose2go/direct.go b/vendor/github.com/dvsekhvalnov/jose2go/direct.go new file mode 100644 index 0000000000..51f0a2f09f --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/direct.go @@ -0,0 +1,39 @@ +package jose + +import ( + "errors" +) + +func init() { + RegisterJwa(new(Direct)) +} + +// Direct (pre-shared) key management algorithm implementation +type Direct struct{ +} + +func (alg *Direct) Name() string { + return DIR +} + +func (alg *Direct) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + + if cek,ok:=key.([]byte); ok { + return cek,[]byte{},nil + } + + return nil,nil,errors.New("Direct.WrapNewKey(): expected key to be '[]byte' array") +} + +func (alg *Direct) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + + if(len(encryptedCek)!=0) { + return nil, errors.New("Direct.Unwrap(): expected empty encrypted CEK") + } + + if cek,ok:=key.([]byte); ok { + return cek,nil + } + + return nil,errors.New("Direct.Unwrap(): expected key to be '[]byte' array") +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/ecdh.go b/vendor/github.com/dvsekhvalnov/jose2go/ecdh.go new file mode 100644 index 0000000000..c3512fb340 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/ecdh.go @@ -0,0 +1,157 @@ +package jose + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "errors" + "fmt" + "github.com/dvsekhvalnov/jose2go/arrays" + "github.com/dvsekhvalnov/jose2go/base64url" + "github.com/dvsekhvalnov/jose2go/kdf" + "github.com/dvsekhvalnov/jose2go/keys/ecc" + "github.com/dvsekhvalnov/jose2go/padding" + "math/big" +) + +func init() { + RegisterJwa(&Ecdh{directAgreement: true}) +} + +// Elliptic curve Diffie–Hellman key management (key agreement) algorithm implementation +type Ecdh struct { + directAgreement bool +} + +func (alg *Ecdh) Name() string { + return ECDH_ES +} + +func (alg *Ecdh) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + + if pubKey, ok := key.(*ecdsa.PublicKey); ok { + + if _, ok := header[alg.idHeader()].(string); !ok { + return nil, nil, errors.New(fmt.Sprintf("Ecdh.WrapNewKey(): expected '%v' param in JWT header, but was not found.", alg.idHeader())) + } + + var d []byte + var x, y *big.Int + + if d, x, y, err = elliptic.GenerateKey(pubKey.Curve, rand.Reader); err != nil { + return nil, nil, err + } + + ephemeral := ecc.NewPrivate(x.Bytes(), y.Bytes(), d) + + xBytes := padding.Align(x.Bytes(), pubKey.Curve.Params().BitSize) + yBytes := padding.Align(y.Bytes(), pubKey.Curve.Params().BitSize) + + epk := map[string]string{ + "kty": "EC", + "x": base64url.Encode(xBytes), + "y": base64url.Encode(yBytes), + "crv": name(pubKey.Curve), + } + + header["epk"] = epk + + return alg.deriveKey(pubKey, ephemeral, cekSizeBits, header), nil, nil + } + + return nil, nil, errors.New("Ecdh.WrapNewKey(): expected key to be '*ecdsa.PublicKey'") +} + +func (alg *Ecdh) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + + if privKey, ok := key.(*ecdsa.PrivateKey); ok { + + var epk map[string]interface{} + + if epk, ok = header["epk"].(map[string]interface{}); !ok { + return nil, errors.New("Ecdh.Unwrap(): expected 'epk' param in JWT header, but was not found.") + } + + if _, ok := header[alg.idHeader()].(string); !ok { + return nil, errors.New(fmt.Sprintf("Ecdh.Unwrap(): expected '%v' param in JWT header, but was not found.", alg.idHeader())) + } + + var x, y, crv string + var xBytes, yBytes []byte + + if x, ok = epk["x"].(string); !ok { + return nil, errors.New("Ecdh.Unwrap(): expects 'epk' key to contain 'x','y' and 'crv' fields, but 'x' was not found.") + } + + if y, ok = epk["y"].(string); !ok { + return nil, errors.New("Ecdh.Unwrap(): expects 'epk' key to contain 'x','y' and 'crv' fields, but 'y' was not found.") + } + + if crv, ok = epk["crv"].(string); !ok { + return nil, errors.New("Ecdh.Unwrap(): expects 'epk' key to contain 'x','y' and 'crv' fields, but 'crv' was not found.") + } + + if crv != "P-256" && crv != "P-384" && crv != "P-521" { + return nil, errors.New(fmt.Sprintf("Ecdh.Unwrap(): unknown or unsupported curve %v", crv)) + } + + if xBytes, err = base64url.Decode(x); err != nil { + return nil, err + } + if yBytes, err = base64url.Decode(y); err != nil { + return nil, err + } + + pubKey := ecc.NewPublic(xBytes, yBytes) + + if !privKey.Curve.IsOnCurve(pubKey.X, pubKey.Y) { + return nil, errors.New(fmt.Sprintf("Ephemeral public key received in header is invalid for reciever's private key.")) + } + + return alg.deriveKey(pubKey, privKey, cekSizeBits, header), nil + } + + return nil, errors.New("Ecdh.Unwrap(): expected key to be '*ecdsa.PrivateKey'") +} + +func (alg *Ecdh) deriveKey(pubKey *ecdsa.PublicKey, privKey *ecdsa.PrivateKey, keySizeBits int, header map[string]interface{}) []byte { + + var enc, apv, apu []byte + var err error + + enc = []byte(header[alg.idHeader()].(string)) + + if a, ok := header["apv"].(string); !ok { + if apv, err = base64url.Decode(a); err != nil { + apv = nil + } + } + + if a, ok := header["apu"].(string); !ok { + if apu, err = base64url.Decode(a); err != nil { + apu = nil + } + } + + z, _ := pubKey.Curve.ScalarMult(pubKey.X, pubKey.Y, privKey.D.Bytes()) + zBytes := padding.Align(z.Bytes(), privKey.Curve.Params().BitSize) + + return kdf.DeriveConcatKDF(keySizeBits, zBytes, prependDatalen(enc), prependDatalen(apu), prependDatalen(apv), arrays.UInt32ToBytes(uint32(keySizeBits)), nil, sha256.New()) +} + +func (alg *Ecdh) idHeader() string { + if alg.directAgreement { + return "enc" + } + + return "alg" +} + +func name(curve elliptic.Curve) string { + return fmt.Sprintf("P-%v", curve.Params().BitSize) +} + +func prependDatalen(bytes []byte) []byte { + return arrays.Concat(arrays.UInt32ToBytes(uint32(len(bytes))), bytes) +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/ecdh_aeskw.go b/vendor/github.com/dvsekhvalnov/jose2go/ecdh_aeskw.go new file mode 100644 index 0000000000..bff348096b --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/ecdh_aeskw.go @@ -0,0 +1,42 @@ +package jose + +func init() { + RegisterJwa(&EcdhAesKW{ keySizeBits: 128, aesKW: &AesKW{ keySizeBits: 128}, ecdh: &Ecdh{directAgreement:false}}) + RegisterJwa(&EcdhAesKW{ keySizeBits: 192, aesKW: &AesKW{ keySizeBits: 192}, ecdh: &Ecdh{directAgreement:false}}) + RegisterJwa(&EcdhAesKW{ keySizeBits: 256, aesKW: &AesKW{ keySizeBits: 256}, ecdh: &Ecdh{directAgreement:false}}) +} + +// Elliptic curve Diffie–Hellman with AES Key Wrap key management algorithm implementation +type EcdhAesKW struct{ + keySizeBits int + aesKW JwaAlgorithm + ecdh JwaAlgorithm +} + +func (alg *EcdhAesKW) Name() string { + switch alg.keySizeBits { + case 128: return ECDH_ES_A128KW + case 192: return ECDH_ES_A192KW + default: return ECDH_ES_A256KW + } +} + +func (alg *EcdhAesKW) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + var kek []byte + + if kek,_,err=alg.ecdh.WrapNewKey(alg.keySizeBits, key, header);err!=nil { + return nil,nil,err + } + + return alg.aesKW.WrapNewKey(cekSizeBits,kek,header) +} + +func (alg *EcdhAesKW) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + var kek []byte + + if kek,err=alg.ecdh.Unwrap(nil, key, alg.keySizeBits, header);err!=nil { + return nil,err + } + + return alg.aesKW.Unwrap(encryptedCek,kek,cekSizeBits,header) +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/ecdsa_using_sha.go b/vendor/github.com/dvsekhvalnov/jose2go/ecdsa_using_sha.go new file mode 100644 index 0000000000..23ac6ee36c --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/ecdsa_using_sha.go @@ -0,0 +1,76 @@ +package jose + +import ( + "crypto/rand" + "math/big" + "crypto/ecdsa" + "errors" + "github.com/dvsekhvalnov/jose2go/arrays" + "github.com/dvsekhvalnov/jose2go/padding" + "fmt" +) + +func init() { + RegisterJws(&EcdsaUsingSha{keySizeBits: 256, hashSizeBits: 256}) + RegisterJws(&EcdsaUsingSha{keySizeBits: 384, hashSizeBits: 384}) + RegisterJws(&EcdsaUsingSha{keySizeBits: 521, hashSizeBits: 512}) +} + +// ECDSA signing algorithm implementation +type EcdsaUsingSha struct{ + keySizeBits int + hashSizeBits int +} + +func (alg *EcdsaUsingSha) Name() string { + switch alg.keySizeBits { + case 256: return ES256 + case 384: return ES384 + default: return ES512 + } +} + +func (alg *EcdsaUsingSha) Verify(securedInput, signature []byte, key interface{}) error { + + if pubKey,ok:=key.(*ecdsa.PublicKey);ok { + + if sizeBits:=pubKey.Curve.Params().BitSize;sizeBits!=alg.keySizeBits { + return errors.New(fmt.Sprintf("EcdsaUsingSha.Verify(): expected key of size %v bits, but was given %v bits.",alg.keySizeBits,sizeBits)) + } + + r:=new(big.Int).SetBytes(signature[:len(signature)/2]) + s:=new(big.Int).SetBytes(signature[len(signature)/2:]) + + if ok:=ecdsa.Verify(pubKey, sha(alg.hashSizeBits, securedInput), r,s); ok { + return nil + } + + return errors.New("EcdsaUsingSha.Verify(): Signature is not valid.") + } + + return errors.New("EcdsaUsingSha.Verify(): expects key to be '*ecdsa.PublicKey'") +} + +func (alg *EcdsaUsingSha) Sign(securedInput []byte, key interface{}) (signature []byte, err error) { + + if privKey,ok := key.(*ecdsa.PrivateKey);ok { + + if sizeBits:=privKey.Curve.Params().BitSize;sizeBits!=alg.keySizeBits { + return nil,errors.New(fmt.Sprintf("EcdsaUsingSha.Sign(): expected key of size %v bits, but was given %v bits.",alg.keySizeBits,sizeBits)) + } + + var r,s *big.Int + + if r,s,err = ecdsa.Sign(rand.Reader, privKey, sha(alg.hashSizeBits, securedInput));err==nil { + + rBytes:=padding.Align(r.Bytes(), alg.keySizeBits) + sBytes:=padding.Align(s.Bytes(), alg.keySizeBits) + + return arrays.Concat(rBytes,sBytes),nil + } + + return nil, err + } + + return nil,errors.New("EcdsaUsingSha.Sign(): expects key to be '*ecdsa.PrivateKey'") +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/hmac.go b/vendor/github.com/dvsekhvalnov/jose2go/hmac.go new file mode 100644 index 0000000000..d3726b77b2 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/hmac.go @@ -0,0 +1,13 @@ +package jose + +import ( + "crypto/hmac" + "hash" +) + +func calculateHmac(keySizeBits int, securedInput []byte, key []byte) []byte { + hasher := hmac.New(func() hash.Hash { return hashAlg(keySizeBits)}, key) + hasher.Write(securedInput) + + return hasher.Sum(nil) +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/hmac_using_sha.go b/vendor/github.com/dvsekhvalnov/jose2go/hmac_using_sha.go new file mode 100644 index 0000000000..e0b664f6a2 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/hmac_using_sha.go @@ -0,0 +1,46 @@ +package jose + +import ( + "crypto/hmac" + "errors" +) + +func init() { + RegisterJws(&HmacUsingSha{keySizeBits: 256}) + RegisterJws(&HmacUsingSha{keySizeBits: 384}) + RegisterJws(&HmacUsingSha{keySizeBits: 512}) +} + +// HMAC with SHA signing algorithm implementation +type HmacUsingSha struct{ + keySizeBits int +} + +func (alg *HmacUsingSha) Name() string { + switch alg.keySizeBits { + case 256: return HS256 + case 384: return HS384 + default: return HS512 + } +} + +func (alg *HmacUsingSha) Verify(securedInput, signature []byte, key interface{}) error { + + actualSig,_ := alg.Sign(securedInput, key) + + if !hmac.Equal(signature, actualSig) { + return errors.New("HmacUsingSha.Verify(): Signature is invalid") + } + + return nil +} + +func (alg *HmacUsingSha) Sign(securedInput []byte, key interface{}) (signature []byte, err error) { + //TODO: assert min key size + + if pubKey,ok:=key.([]byte); ok { + return calculateHmac(alg.keySizeBits, securedInput, pubKey),nil + } + + return nil,errors.New("HmacUsingSha.Sign(): expects key to be '[]byte' array") +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/jose.go b/vendor/github.com/dvsekhvalnov/jose2go/jose.go new file mode 100644 index 0000000000..3549a91863 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/jose.go @@ -0,0 +1,485 @@ +// Package jose provides high level functions for producing (signing, encrypting and +// compressing) or consuming (decoding) Json Web Tokens using Java Object Signing and Encryption spec +package jose + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/dvsekhvalnov/jose2go/compact" +) + +const ( + NONE = "none" //plaintext (unprotected) without signature / encryption + + HS256 = "HS256" //HMAC using SHA-256 hash + HS384 = "HS384" //HMAC using SHA-384 hash + HS512 = "HS512" //HMAC using SHA-512 hash + RS256 = "RS256" //RSASSA-PKCS-v1_5 using SHA-256 hash + RS384 = "RS384" //RSASSA-PKCS-v1_5 using SHA-384 hash + RS512 = "RS512" //RSASSA-PKCS-v1_5 using SHA-512 hash + PS256 = "PS256" //RSASSA-PSS using SHA-256 hash + PS384 = "PS384" //RSASSA-PSS using SHA-384 hash + PS512 = "PS512" //RSASSA-PSS using SHA-512 hash + ES256 = "ES256" //ECDSA using P-256 curve and SHA-256 hash + ES384 = "ES384" //ECDSA using P-384 curve and SHA-384 hash + ES512 = "ES512" //ECDSA using P-521 curve and SHA-512 hash + + A128CBC_HS256 = "A128CBC-HS256" //AES in CBC mode with PKCS #5 (NIST.800-38A) padding with HMAC using 256 bit key + A192CBC_HS384 = "A192CBC-HS384" //AES in CBC mode with PKCS #5 (NIST.800-38A) padding with HMAC using 384 bit key + A256CBC_HS512 = "A256CBC-HS512" //AES in CBC mode with PKCS #5 (NIST.800-38A) padding with HMAC using 512 bit key + A128GCM = "A128GCM" //AES in GCM mode with 128 bit key + A192GCM = "A192GCM" //AES in GCM mode with 192 bit key + A256GCM = "A256GCM" //AES in GCM mode with 256 bit key + + DIR = "dir" //Direct use of pre-shared symmetric key + RSA1_5 = "RSA1_5" //RSAES with PKCS #1 v1.5 padding, RFC 3447 + RSA_OAEP = "RSA-OAEP" //RSAES using Optimal Assymetric Encryption Padding, RFC 3447 + RSA_OAEP_256 = "RSA-OAEP-256" //RSAES using Optimal Assymetric Encryption Padding with SHA-256, RFC 3447 + A128KW = "A128KW" //AES Key Wrap Algorithm using 128 bit keys, RFC 3394 + A192KW = "A192KW" //AES Key Wrap Algorithm using 192 bit keys, RFC 3394 + A256KW = "A256KW" //AES Key Wrap Algorithm using 256 bit keys, RFC 3394 + A128GCMKW = "A128GCMKW" //AES GCM Key Wrap Algorithm using 128 bit keys + A192GCMKW = "A192GCMKW" //AES GCM Key Wrap Algorithm using 192 bit keys + A256GCMKW = "A256GCMKW" //AES GCM Key Wrap Algorithm using 256 bit keys + PBES2_HS256_A128KW = "PBES2-HS256+A128KW" //Password Based Encryption using PBES2 schemes with HMAC-SHA and AES Key Wrap using 128 bit key + PBES2_HS384_A192KW = "PBES2-HS384+A192KW" //Password Based Encryption using PBES2 schemes with HMAC-SHA and AES Key Wrap using 192 bit key + PBES2_HS512_A256KW = "PBES2-HS512+A256KW" //Password Based Encryption using PBES2 schemes with HMAC-SHA and AES Key Wrap using 256 bit key + ECDH_ES = "ECDH-ES" //Elliptic Curve Diffie Hellman key agreement + ECDH_ES_A128KW = "ECDH-ES+A128KW" //Elliptic Curve Diffie Hellman key agreement with AES Key Wrap using 128 bit key + ECDH_ES_A192KW = "ECDH-ES+A192KW" //Elliptic Curve Diffie Hellman key agreement with AES Key Wrap using 192 bit key + ECDH_ES_A256KW = "ECDH-ES+A256KW" //Elliptic Curve Diffie Hellman key agreement with AES Key Wrap using 256 bit key + + DEF = "DEF" //DEFLATE compression, RFC 1951 +) + +var jwsHashers = map[string]JwsAlgorithm{} +var jweEncryptors = map[string]JweEncryption{} +var jwaAlgorithms = map[string]JwaAlgorithm{} +var jwcCompressors = map[string]JwcAlgorithm{} + +// RegisterJwe register new encryption algorithm +func RegisterJwe(alg JweEncryption) { + jweEncryptors[alg.Name()] = alg +} + +// RegisterJwa register new key management algorithm +func RegisterJwa(alg JwaAlgorithm) { + jwaAlgorithms[alg.Name()] = alg +} + +// RegisterJws register new signing algorithm +func RegisterJws(alg JwsAlgorithm) { + jwsHashers[alg.Name()] = alg +} + +// RegisterJwc register new compression algorithm +func RegisterJwc(alg JwcAlgorithm) { + jwcCompressors[alg.Name()] = alg +} + +// DeregisterJwa deregister existing key management algorithm +func DeregisterJwa(alg string) JwaAlgorithm { + jwa := jwaAlgorithms[alg] + + delete(jwaAlgorithms, alg) + + return jwa +} + +// DeregisterJws deregister existing signing algorithm +func DeregisterJws(alg string) JwsAlgorithm { + jws := jwsHashers[alg] + + delete(jwsHashers, alg) + + return jws +} + +// DeregisterJws deregister existing encryption algorithm +func DeregisterJwe(alg string) JweEncryption { + jwe := jweEncryptors[alg] + + delete(jweEncryptors, alg) + + return jwe +} + +// DeregisterJwc deregister existing compression algorithm +func DeregisterJwc(alg string) JwcAlgorithm { + jwc := jwcCompressors[alg] + + delete(jwcCompressors, alg) + + return jwc +} + +// JweEncryption is a contract for implementing encryption algorithm +type JweEncryption interface { + Encrypt(aad, plainText, cek []byte) (iv, cipherText, authTag []byte, err error) + Decrypt(aad, cek, iv, cipherText, authTag []byte) (plainText []byte, err error) + KeySizeBits() int + Name() string +} + +// JwaAlgorithm is a contract for implementing key management algorithm +type JwaAlgorithm interface { + WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) + Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) + Name() string +} + +// JwsAlgorithm is a contract for implementing signing algorithm +type JwsAlgorithm interface { + Verify(securedInput, signature []byte, key interface{}) error + Sign(securedInput []byte, key interface{}) (signature []byte, err error) + Name() string +} + +// JwcAlgorithm is a contract for implementing compression algorithm +type JwcAlgorithm interface { + Compress(plainText []byte) []byte + Decompress(compressedText []byte) []byte + Name() string +} + +func Zip(alg string) func(cfg *JoseConfig) { + return func(cfg *JoseConfig) { + cfg.CompressionAlg = alg + } +} + +func Header(name string, value interface{}) func(cfg *JoseConfig) { + return func(cfg *JoseConfig) { + cfg.Headers[name] = value + } +} + +func Headers(headers map[string]interface{}) func(cfg *JoseConfig) { + return func(cfg *JoseConfig) { + for k, v := range headers { + cfg.Headers[k] = v + } + } +} + +type JoseConfig struct { + CompressionAlg string + Headers map[string]interface{} +} + +// Sign produces signed JWT token given arbitrary string payload, signature algorithm to use (see constants for list of supported algs), signing key and extra options (see option functions) +// Signing key is of different type for different signing alg, see specific +// signing alg implementation documentation. +// +// It returns 3 parts signed JWT token as string and not nil error if something went wrong. +func Sign(payload string, signingAlg string, key interface{}, options ...func(*JoseConfig)) (token string, err error) { + return SignBytes([]byte(payload), signingAlg, key, options...) +} + +// Sign produces signed JWT token given arbitrary binary payload, signature algorithm to use (see constants for list of supported algs), signing key and extra options (see option functions) +// Signing key is of different type for different signing alg, see specific +// signing alg implementation documentation. +// +// It returns 3 parts signed JWT token as string and not nil error if something went wrong. +func SignBytes(payload []byte, signingAlg string, key interface{}, options ...func(*JoseConfig)) (token string, err error) { + if signer, ok := jwsHashers[signingAlg]; ok { + + cfg := &JoseConfig{CompressionAlg: "", Headers: make(map[string]interface{})} + + //apply extra options + for _, option := range options { + option(cfg) + } + + //make sure defaults and requires are managed by us + cfg.Headers["alg"] = signingAlg + + paloadBytes := payload + var header []byte + var signature []byte + + if header, err = json.Marshal(cfg.Headers); err == nil { + securedInput := []byte(compact.Serialize(header, paloadBytes)) + + if signature, err = signer.Sign(securedInput, key); err == nil { + return compact.Serialize(header, paloadBytes, signature), nil + } + } + + return "", err + } + + return "", errors.New(fmt.Sprintf("jwt.Sign(): unknown algorithm: '%v'", signingAlg)) +} + +// Encrypt produces encrypted JWT token given arbitrary string payload, key management and encryption algorithms to use (see constants for list of supported algs) and management key. +// Management key is of different type for different key management alg, see specific +// key management alg implementation documentation. +// +// It returns 5 parts encrypted JWT token as string and not nil error if something went wrong. +func Encrypt(payload string, alg string, enc string, key interface{}, options ...func(*JoseConfig)) (token string, err error) { + return EncryptBytes([]byte(payload), alg, enc, key, options...) +} + +// Encrypt produces encrypted JWT token given arbitrary binary payload, key management and encryption algorithms to use (see constants for list of supported algs) and management key. +// Management key is of different type for different key management alg, see specific +// key management alg implementation documentation. +// +// It returns 5 parts encrypted JWT token as string and not nil error if something went wrong. +func EncryptBytes(payload []byte, alg string, enc string, key interface{}, options ...func(*JoseConfig)) (token string, err error) { + + cfg := &JoseConfig{CompressionAlg: "", Headers: make(map[string]interface{})} + + //apply extra options + for _, option := range options { + option(cfg) + } + + //make sure required headers are managed by us + cfg.Headers["alg"] = alg + cfg.Headers["enc"] = enc + + byteContent := payload + + if cfg.CompressionAlg != "" { + if zipAlg, ok := jwcCompressors[cfg.CompressionAlg]; ok { + byteContent = zipAlg.Compress([]byte(payload)) + cfg.Headers["zip"] = cfg.CompressionAlg + } else { + return "", errors.New(fmt.Sprintf("jwt.Compress(): Unknown compression method '%v'", cfg.CompressionAlg)) + } + + } else { + delete(cfg.Headers, "zip") //we not allow to manage 'zip' header manually for encryption + } + + return encrypt(byteContent, cfg.Headers, key) +} + +// This method is DEPRICATED and subject to be removed in next version. +// Use Encrypt(..) with Zip option instead. +// +// Compress produces encrypted & comressed JWT token given arbitrary payload, key management , encryption and compression algorithms to use (see constants for list of supported algs) and management key. +// Management key is of different type for different key management alg, see specific +// key management alg implementation documentation. +// +// It returns 5 parts encrypted & compressed JWT token as string and not nil error if something went wrong. +func Compress(payload string, alg string, enc string, zip string, key interface{}) (token string, err error) { + + if zipAlg, ok := jwcCompressors[zip]; ok { + compressed := zipAlg.Compress([]byte(payload)) + + jwtHeader := map[string]interface{}{ + "enc": enc, + "alg": alg, + "zip": zip, + } + + return encrypt(compressed, jwtHeader, key) + } + + return "", errors.New(fmt.Sprintf("jwt.Compress(): Unknown compression method '%v'", zip)) +} + +// Decode verifies, decrypts and decompresses given JWT token using management key. +// Management key is of different type for different key management or signing algorithms, see specific alg implementation documentation. +// +// Returns decoded payload as a string, headers and not nil error if something went wrong. +func Decode(token string, key interface{}) (string, map[string]interface{}, error) { + + payload, headers, err := DecodeBytes(token, key) + + if err != nil { + return "", nil, err + } + + return string(payload), headers, nil +} + +// Decode verifies, decrypts and decompresses given JWT token using management key. +// Management key is of different type for different key management or signing algorithms, see specific alg implementation documentation. +// +// Returns decoded payload as a raw bytes, headers and not nil error if something went wrong. +func DecodeBytes(token string, key interface{}) ([]byte, map[string]interface{}, error) { + parts, err := compact.Parse(token) + + if err != nil { + return nil, nil, err + } + + if len(parts) == 3 { + return verify(parts, key) + } + + if len(parts) == 5 { + return decrypt(parts, key) + } + + return nil, nil, errors.New(fmt.Sprintf("jwt.DecodeBytes() expects token of 3 or 5 parts, but was given: %v parts", len(parts))) +} + +func encrypt(payload []byte, jwtHeader map[string]interface{}, key interface{}) (token string, err error) { + var ok bool + var keyMgmtAlg JwaAlgorithm + var encAlg JweEncryption + + alg := jwtHeader["alg"].(string) + enc := jwtHeader["enc"].(string) + + if keyMgmtAlg, ok = jwaAlgorithms[alg]; !ok { + return "", errors.New(fmt.Sprintf("jwt.encrypt(): Unknown key management algorithm '%v'", alg)) + } + + if encAlg, ok = jweEncryptors[enc]; !ok { + return "", errors.New(fmt.Sprintf("jwt.encrypt(): Unknown encryption algorithm '%v'", enc)) + } + + var cek, encryptedCek, header, iv, cipherText, authTag []byte + + if cek, encryptedCek, err = keyMgmtAlg.WrapNewKey(encAlg.KeySizeBits(), key, jwtHeader); err != nil { + return "", err + } + + if header, err = json.Marshal(jwtHeader); err != nil { + return "", err + } + + if iv, cipherText, authTag, err = encAlg.Encrypt([]byte(compact.Serialize(header)), payload, cek); err != nil { + return "", err + } + + return compact.Serialize(header, encryptedCek, iv, cipherText, authTag), nil +} + +func verify(parts [][]byte, key interface{}) (plainText []byte, headers map[string]interface{}, err error) { + + header, payload, signature := parts[0], parts[1], parts[2] + + secured := []byte(compact.Serialize(header, payload)) + + var jwtHeader map[string]interface{} + + if err = json.Unmarshal(header, &jwtHeader); err != nil { + return nil, nil, err + } + + if alg, ok := jwtHeader["alg"].(string); ok { + if verifier, ok := jwsHashers[alg]; ok { + if key, err = retrieveActualKey(jwtHeader, string(payload), key); err != nil { + return nil, nil, err + } + + if err = verifier.Verify(secured, signature, key); err == nil { + return payload, jwtHeader, nil + } + + return nil, nil, err + } + + return nil, nil, errors.New(fmt.Sprintf("jwt.Decode(): Unknown algorithm: '%v'", alg)) + } + + return nil, nil, errors.New(fmt.Sprint("jwt.Decode(): required 'alg' header is missing or of invalid type")) +} + +func decrypt(parts [][]byte, key interface{}) (plainText []byte, headers map[string]interface{}, err error) { + + header, encryptedCek, iv, cipherText, authTag := parts[0], parts[1], parts[2], parts[3], parts[4] + + var jwtHeader map[string]interface{} + + if e := json.Unmarshal(header, &jwtHeader); e != nil { + return nil, nil, e + } + + var keyMgmtAlg JwaAlgorithm + var encAlg JweEncryption + var zipAlg JwcAlgorithm + var cek, plainBytes []byte + var ok bool + var alg, enc string + + if alg, ok = jwtHeader["alg"].(string); !ok { + return nil, nil, errors.New(fmt.Sprint("jwt.Decode(): required 'alg' header is missing or of invalid type")) + } + + if enc, ok = jwtHeader["enc"].(string); !ok { + return nil, nil, errors.New(fmt.Sprint("jwt.Decode(): required 'enc' header is missing or of invalid type")) + } + + aad := []byte(compact.Serialize(header)) + + if keyMgmtAlg, ok = jwaAlgorithms[alg]; ok { + if encAlg, ok = jweEncryptors[enc]; ok { + + if key, err = retrieveActualKey(jwtHeader, string(cipherText), key); err != nil { + return nil, nil, err + } + + if cek, err = keyMgmtAlg.Unwrap(encryptedCek, key, encAlg.KeySizeBits(), jwtHeader); err == nil { + if plainBytes, err = encAlg.Decrypt(aad, cek, iv, cipherText, authTag); err == nil { + + if zip, compressed := jwtHeader["zip"].(string); compressed { + + if zipAlg, ok = jwcCompressors[zip]; !ok { + return nil, nil, errors.New(fmt.Sprintf("jwt.decrypt(): Unknown compression algorithm '%v'", zip)) + } + + plainBytes = zipAlg.Decompress(plainBytes) + } + + return plainBytes, jwtHeader, nil + } + + return nil, nil, err + } + + return nil, nil, err + } + + return nil, nil, errors.New(fmt.Sprintf("jwt.decrypt(): Unknown encryption algorithm '%v'", enc)) + } + + return nil, nil, errors.New(fmt.Sprintf("jwt.decrypt(): Unknown key management algorithm '%v'", alg)) +} + +func retrieveActualKey(headers map[string]interface{}, payload string, key interface{}) (interface{}, error) { + if keyCallback, ok := key.(func(headers map[string]interface{}, payload string) interface{}); ok { + result := keyCallback(headers, payload) + + if err, ok := result.(error); ok { + return nil, err + } + + return result, nil + } + + return key, nil +} + +func Alg(key interface{}, jws string) func(headers map[string]interface{}, payload string) interface{} { + return func(headers map[string]interface{}, payload string) interface{} { + alg := headers["alg"].(string) + + if jws == alg { + return key + } + + return errors.New("Expected alg to be '" + jws + "' but got '" + alg + "'") + } +} + +func Enc(key interface{}, jwa string, jwe string) func(headers map[string]interface{}, payload string) interface{} { + return func(headers map[string]interface{}, payload string) interface{} { + alg := headers["alg"].(string) + enc := headers["enc"].(string) + + if jwa == alg && jwe == enc { + return key + } + + return errors.New("Expected alg to be '" + jwa + "' and enc to be '" + jwe + "' but got '" + alg + "' and '" + enc + "'") + } +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/kdf/nist_sp800_56a.go b/vendor/github.com/dvsekhvalnov/jose2go/kdf/nist_sp800_56a.go new file mode 100644 index 0000000000..dd76790aed --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/kdf/nist_sp800_56a.go @@ -0,0 +1,43 @@ +package kdf + +import ( + "hash" + "math" + "github.com/dvsekhvalnov/jose2go/arrays" +) + +const ( + MaxInt = int(^uint(0)>>1); +) + +// DeriveConcatKDF implements NIST SP 800-56A Concatenation Key Derivation Function. Derives +// key material of keydatalen bits size given Z (sharedSecret), OtherInfo (AlgorithmID | +// PartyUInfo | PartyVInfo | SuppPubInfo | SuppPrivInfo) and hash function +func DeriveConcatKDF(keydatalen int, sharedSecret, algId, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo []byte, h hash.Hash) []byte { + + otherInfo := arrays.Concat(algId, partyUInfo, partyVInfo, suppPubInfo, suppPrivInfo) + + keyLenBytes := keydatalen >> 3 + + reps := int(math.Ceil(float64(keyLenBytes) / float64(h.Size()))) + + if reps > MaxInt { + panic("kdf.DeriveConcatKDF: too much iterations (more than 2^32-1).") + } + + dk:=make([]byte, 0, keyLenBytes) + + for counter := 1;counter <= reps;counter++ { + h.Reset() + + counterBytes:=arrays.UInt32ToBytes(uint32(counter)) + + h.Write(counterBytes) + h.Write(sharedSecret) + h.Write(otherInfo) + + dk = h.Sum(dk) + } + + return dk[:keyLenBytes] +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/kdf/pbkdf2.go b/vendor/github.com/dvsekhvalnov/jose2go/kdf/pbkdf2.go new file mode 100644 index 0000000000..aec58e76c7 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/kdf/pbkdf2.go @@ -0,0 +1,63 @@ +// package kdf contains implementations of various key derivation functions +package kdf + +import ( + "crypto/hmac" + "fmt" + "hash" + "math" + + "github.com/dvsekhvalnov/jose2go/arrays" +) + +// DerivePBKDF2 implements Password Based Key Derivation Function 2, RFC 2898. Derives key of keyBitLength size, given password, salt, iteration count and hash function +func DerivePBKDF2(password, salt []byte, iterationCount, keyBitLength int, h func() hash.Hash) []byte { + + prf := hmac.New(h, password) + hLen := prf.Size() + dkLen := keyBitLength >> 3 //size of derived key in bytes + + l := int(math.Ceil(float64(dkLen) / float64(hLen))) // l = CEIL (dkLen / hLen) + r := dkLen - (l-1)*hLen + + // 1. If dkLen > (2^32 - 1) * hLen, output "derived key too long" and stop. + if dkLen > MaxInt { + panic(fmt.Sprintf("kdf.DerivePBKDF2: expects derived key size to be not more that (2^32-1) bits, but was requested %v bits.", keyBitLength)) + } + + dk := make([]byte, 0, dkLen) + + for i := 0; i < l; i++ { + + t := f(salt, iterationCount, i+1, prf) // T_l = F (P, S, c, l) + + if i == (l - 1) { + t = t[:r] + } // truncate last block to r bits + + dk = append(dk, t...) // DK = T_1 || T_2 || ... || T_l<0..r-1> + } + + return dk +} + +func f(salt []byte, iterationCount, blockIndex int, prf hash.Hash) []byte { + + prf.Reset() + prf.Write(salt) + prf.Write(arrays.UInt32ToBytes(uint32(blockIndex))) + + u := prf.Sum(nil) // U_1 = PRF (P, S || INT (i)) + + result := u + + for i := 2; i <= iterationCount; i++ { + prf.Reset() + prf.Write(u) + + u = prf.Sum(nil) // U_c = PRF (P, U_{c-1}) . + result = arrays.Xor(result, u) // U_1 \xor U_2 \xor ... \xor U_c + } + + return result +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_cert.pem b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_cert.pem new file mode 100644 index 0000000000..384cb4691b --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_cert.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICJjCCAc6gAwIBAgIJAOCtH/xv+cfpMAkGByqGSM49BAEwRTELMAkGA1UEBhMC +QVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdpZGdp +dHMgUHR5IEx0ZDAeFw0xNDA4MTIxMTU5MTVaFw0xODA1MDgxMTU5MTVaMEUxCzAJ +BgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5l +dCBXaWRnaXRzIFB0eSBMdGQwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASqGi1p +Eudyh+Nw//zeAGIYSQG/H/q/x6Xz2TIg3GN4hbD5BS1f4/vEitpui+TpmtJyggzo +x5D55N9kjq0X/hyyo4GnMIGkMB0GA1UdDgQWBBQvGSEecx9JdDECRIorVpeWy7oA +ujB1BgNVHSMEbjBsgBQvGSEecx9JdDECRIorVpeWy7oAuqFJpEcwRTELMAkGA1UE +BhMCQVUxEzARBgNVBAgTClNvbWUtU3RhdGUxITAfBgNVBAoTGEludGVybmV0IFdp +ZGdpdHMgUHR5IEx0ZIIJAOCtH/xv+cfpMAwGA1UdEwQFMAMBAf8wCQYHKoZIzj0E +AQNHADBEAiAUQheZrGjbsy6PfpWGZEhTFzqvBVXtbqtz+6aTkOCrCQIgLfvw9C+0 +SDn/abV4NtgYOM0OLkoNRTCIzzguHxhhaJ4= +-----END CERTIFICATE----- diff --git a/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.key b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.key new file mode 100644 index 0000000000..399b84b295 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.key @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIA/32XgQpS/tKRhw8jSdr8ivBmMyD/vbG5KT3s3XPArcoAoGCCqGSM49 +AwEHoUQDQgAEqhotaRLncofjcP/83gBiGEkBvx/6v8el89kyINxjeIWw+QUtX+P7 +xIrabovk6ZrScoIM6MeQ+eTfZI6tF/4csg== +-----END EC PRIVATE KEY----- diff --git a/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.pem b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.pem new file mode 100644 index 0000000000..5247be2bd2 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_private.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgD/fZeBClL+0pGHDy +NJ2vyK8GYzIP+9sbkpPezdc8CtyhRANCAASqGi1pEudyh+Nw//zeAGIYSQG/H/q/ +x6Xz2TIg3GN4hbD5BS1f4/vEitpui+TpmtJyggzox5D55N9kjq0X/hyy +-----END PRIVATE KEY----- diff --git a/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_public.key b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_public.key new file mode 100644 index 0000000000..bc2345d641 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ec_public.key @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqhotaRLncofjcP/83gBiGEkBvx/6 +v8el89kyINxjeIWw+QUtX+P7xIrabovk6ZrScoIM6MeQ+eTfZI6tF/4csg== +-----END PUBLIC KEY----- diff --git a/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ecc.go b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ecc.go new file mode 100644 index 0000000000..486d81650a --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/keys/ecc/ecc.go @@ -0,0 +1,92 @@ +//package ecc provides helpers for creating elliptic curve leys +package ecc + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/x509" + "encoding/pem" + "errors" + "math/big" +) + +// ReadPublic loads ecdsa.PublicKey from given PKCS1 X509 or PKIX blobs +func ReadPublic(raw []byte) (key *ecdsa.PublicKey, err error) { + var encoded *pem.Block + + if encoded, _ = pem.Decode(raw); encoded == nil { + return nil, errors.New("Ecc.ReadPublic(): Key must be PEM encoded PKCS1 X509 certificate or PKIX EC public key") + } + + var parsedKey interface{} + var cert *x509.Certificate + + if parsedKey, err = x509.ParsePKIXPublicKey(encoded.Bytes); err != nil { + if cert, err = x509.ParseCertificate(encoded.Bytes); err != nil { + return nil, err + } + + parsedKey = cert.PublicKey + } + + var ok bool + + if key, ok = parsedKey.(*ecdsa.PublicKey); !ok { + return nil, errors.New("Ecc.ReadPublic(): Key is not a valid *ecdsa.PublicKey") + } + + return key, nil +} + +// ReadPrivate loads ecdsa.PrivateKey from given PKCS1 or PKCS8 blobs +func ReadPrivate(raw []byte) (key *ecdsa.PrivateKey, err error) { + var encoded *pem.Block + + if encoded, _ = pem.Decode(raw); encoded == nil { + return nil, errors.New("Ecc.ReadPrivate(): Key must be PEM encoded PKCS1 or PKCS8 EC private key") + } + + var parsedKey interface{} + + if parsedKey, err = x509.ParseECPrivateKey(encoded.Bytes); err != nil { + if parsedKey, err = x509.ParsePKCS8PrivateKey(encoded.Bytes); err != nil { + return nil, err + } + } + + var ok bool + + if key, ok = parsedKey.(*ecdsa.PrivateKey); !ok { + return nil, errors.New("Ecc.ReadPrivate(): Key is not valid *ecdsa.PrivateKey") + } + + return key, nil +} + +// NewPublic constructs ecdsa.PublicKey from given (X,Y) +func NewPublic(x, y []byte) *ecdsa.PublicKey { + return &ecdsa.PublicKey{Curve: curve(len(x)), + X: new(big.Int).SetBytes(x), + Y: new(big.Int).SetBytes(y)} +} + +// NewPrivate constructs ecdsa.PrivateKey from given (X,Y) and D +func NewPrivate(x, y, d []byte) *ecdsa.PrivateKey { + return &ecdsa.PrivateKey{D: new(big.Int).SetBytes(d), + PublicKey: ecdsa.PublicKey{Curve: curve(len(x)), + X: new(big.Int).SetBytes(x), + Y: new(big.Int).SetBytes(y)}} +} + +func curve(size int) elliptic.Curve { + switch size { + case 31, 32: + return elliptic.P256() + case 48: + return elliptic.P384() + case 65, 66: + return elliptic.P521() //adjust for P-521 curve, which can be 65 or 66 bytes + default: + return nil //unsupported curve + } +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/padding/align.go b/vendor/github.com/dvsekhvalnov/jose2go/padding/align.go new file mode 100644 index 0000000000..0ef601f94c --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/padding/align.go @@ -0,0 +1,23 @@ +// package padding provides various padding algorithms +package padding + +import ( + "bytes" +) + +// Align left pads given byte array with zeros till it have at least bitSize length. +func Align(data []byte, bitSize int) []byte { + + actual:=len(data) + required:=bitSize >> 3 + + if (bitSize % 8) > 0 { + required++ //extra byte if needed + } + + if (actual >= required) { + return data + } + + return append(bytes.Repeat([]byte{0}, required-actual), data...) +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/padding/pkcs7.go b/vendor/github.com/dvsekhvalnov/jose2go/padding/pkcs7.go new file mode 100644 index 0000000000..2f64e7e1ed --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/padding/pkcs7.go @@ -0,0 +1,38 @@ +package padding + +import ( + "bytes" +) + +// AddPkcs7 pads given byte array using pkcs7 padding schema till it has blockSize length in bytes +func AddPkcs7(data []byte, blockSize int) []byte { + + var paddingCount int + + if paddingCount = blockSize - (len(data) % blockSize);paddingCount == 0 { + paddingCount=blockSize + } + + return append(data, bytes.Repeat([]byte{byte(paddingCount)}, paddingCount)...) +} + +// RemovePkcs7 removes pkcs7 padding from previously padded byte array +func RemovePkcs7(padded []byte, blockSize int) []byte { + + dataLen:=len(padded) + paddingCount:=int(padded[dataLen-1]) + + if(paddingCount > blockSize || paddingCount <= 0) { + return padded //data is not padded (or not padded correctly), return as is + } + + padding := padded[dataLen-paddingCount : dataLen-1] + + for _, b := range padding { + if int(b) != paddingCount { + return padded //data is not padded (or not padded correcly), return as is + } + } + + return padded[:len(padded)-paddingCount] //return data - padding +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/pbse2_hmac_aeskw.go b/vendor/github.com/dvsekhvalnov/jose2go/pbse2_hmac_aeskw.go new file mode 100644 index 0000000000..2915ae6be4 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/pbse2_hmac_aeskw.go @@ -0,0 +1,142 @@ +package jose + +import ( + "crypto/sha256" + "crypto/sha512" + "errors" + "fmt" + "hash" + + "github.com/dvsekhvalnov/jose2go/arrays" + "github.com/dvsekhvalnov/jose2go/base64url" + "github.com/dvsekhvalnov/jose2go/kdf" +) + +func init() { + RegisterJwa(NewPbse2HmacAesKWAlg(128, 1300000, 0)) + RegisterJwa(NewPbse2HmacAesKWAlg(192, 950000, 0)) + RegisterJwa(NewPbse2HmacAesKWAlg(256, 600000, 0)) +} + +// PBSE2 with HMAC key management algorithm implementation +type Pbse2HmacAesKW struct { + keySizeBits int + aesKW JwaAlgorithm + maxIterations int64 + minIterations int64 +} + +func NewPbse2HmacAesKWAlg(keySize int, maxIters int64, minIters int64) JwaAlgorithm { + switch keySize { + case 128: + return &Pbse2HmacAesKW{keySizeBits: 128, maxIterations: maxIters, minIterations: minIters, aesKW: &AesKW{keySizeBits: 128}} + case 192: + return &Pbse2HmacAesKW{keySizeBits: 192, maxIterations: maxIters, minIterations: minIters, aesKW: &AesKW{keySizeBits: 192}} + default: + return &Pbse2HmacAesKW{keySizeBits: 256, maxIterations: maxIters, minIterations: minIters, aesKW: &AesKW{keySizeBits: 256}} + } +} + +func (alg *Pbse2HmacAesKW) Name() string { + switch alg.keySizeBits { + case 128: + return PBES2_HS256_A128KW + case 192: + return PBES2_HS384_A192KW + default: + return PBES2_HS512_A256KW + } +} + +func (alg *Pbse2HmacAesKW) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + if passphrase, ok := key.(string); ok { + + algId := []byte(header["alg"].(string)) + + iterationCount := 8192 + var saltInput []byte + + if saltInput, err = arrays.Random(12); err != nil { + return nil, nil, err + } + + // use user provided iteration counts if any + if p2c, ok := header["p2c"].(int); ok { + iterationCount = p2c + } + + if int64(iterationCount) > alg.maxIterations { + return nil, nil, errors.New( + fmt.Sprintf("Pbse2HmacAesKW.Unwrap(): expected 'p2c' to be less than %v but got %v", alg.maxIterations, iterationCount)) + } + + if int64(iterationCount) < alg.minIterations { + return nil, nil, errors.New( + fmt.Sprintf("Pbse2HmacAesKW.Unwrap(): expected 'p2c' to be higher than %v but got %v", alg.minIterations, iterationCount)) + } + + header["p2c"] = iterationCount + header["p2s"] = base64url.Encode(saltInput) + + salt := arrays.Concat(algId, []byte{0}, saltInput) + + kek := kdf.DerivePBKDF2([]byte(passphrase), salt, iterationCount, alg.keySizeBits, alg.prf) + return alg.aesKW.WrapNewKey(cekSizeBits, kek, header) + } + + return nil, nil, errors.New("Pbse2HmacAesKW.WrapNewKey(): expected key to be 'string' array") +} + +func (alg *Pbse2HmacAesKW) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + + if passphrase, ok := key.(string); ok { + + var p2s string + var p2c float64 + + if p2c, ok = header["p2c"].(float64); !ok { + return nil, errors.New("Pbse2HmacAesKW.Unwrap(): expected 'p2c' param in JWT header, but was not found.") + } + + if int64(p2c) > alg.maxIterations { + return nil, errors.New( + fmt.Sprintf("Pbse2HmacAesKW.Unwrap(): expected 'p2c' to be less than %v but got %v", alg.maxIterations, p2c)) + } + + if int64(p2c) < alg.minIterations { + return nil, errors.New( + fmt.Sprintf("Pbse2HmacAesKW.Unwrap(): expected 'p2c' to be higher than %v but got %v", alg.minIterations, p2c)) + } + + if p2s, ok = header["p2s"].(string); !ok { + return nil, errors.New("Pbse2HmacAesKW.Unwrap(): expected 'p2s' param in JWT header, but was not found") + } + + var saltInput []byte + + algId := []byte(header["alg"].(string)) + + if saltInput, err = base64url.Decode(p2s); err != nil { + return nil, err + } + + salt := arrays.Concat(algId, []byte{0}, saltInput) + + kek := kdf.DerivePBKDF2([]byte(passphrase), salt, int(p2c), alg.keySizeBits, alg.prf) + + return alg.aesKW.Unwrap(encryptedCek, kek, cekSizeBits, header) + } + + return nil, errors.New("Pbse2HmacAesKW.Unwrap(): expected key to be 'string' array") +} + +func (alg *Pbse2HmacAesKW) prf() hash.Hash { + switch alg.keySizeBits { + case 128: + return sha256.New() + case 192: + return sha512.New384() + default: + return sha512.New() + } +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/plaintext.go b/vendor/github.com/dvsekhvalnov/jose2go/plaintext.go new file mode 100644 index 0000000000..761ce5ad9b --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/plaintext.go @@ -0,0 +1,38 @@ +package jose + +import ( + "errors" +) + +// Plaintext (no signing) signing algorithm implementation +type Plaintext struct{} + +func init() { + RegisterJws(new(Plaintext)) +} + +func (alg *Plaintext) Name() string { + return NONE +} + +func (alg *Plaintext) Verify(securedInput []byte, signature []byte, key interface{}) error { + + if key != nil { + return errors.New("Plaintext.Verify() expects key to be nil") + } + + if len(signature) != 0 { + return errors.New("Plaintext.Verify() expects signature to be empty.") + } + + return nil +} + +func (alg *Plaintext) Sign(securedInput []byte, key interface{}) (signature []byte, err error) { + + if key != nil { + return nil, errors.New("Plaintext.Verify() expects key to be nil") + } + + return []byte{}, nil +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/rsa_oaep.go b/vendor/github.com/dvsekhvalnov/jose2go/rsa_oaep.go new file mode 100644 index 0000000000..b0d1b52a23 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/rsa_oaep.go @@ -0,0 +1,57 @@ +package jose + +import ( + "errors" + "crypto/rsa" + "crypto/rand" + "hash" + "crypto/sha1" + "crypto/sha256" + "github.com/dvsekhvalnov/jose2go/arrays" +) + +// RS-AES using OAEP key management algorithm implementation +func init() { + RegisterJwa(&RsaOaep {shaSizeBits:1}) + RegisterJwa(&RsaOaep {shaSizeBits:256}) +} + +type RsaOaep struct{ + shaSizeBits int + // func shaF() hash.Hash +} + +func (alg *RsaOaep) Name() string { + switch alg.shaSizeBits { + case 1: return RSA_OAEP + default: return RSA_OAEP_256 + } +} + +func (alg *RsaOaep) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + if pubKey,ok:=key.(*rsa.PublicKey);ok { + if cek,err = arrays.Random(cekSizeBits>>3);err==nil { + encryptedCek,err=rsa.EncryptOAEP(alg.sha(),rand.Reader,pubKey,cek,nil) + return + } + + return nil,nil,err + } + + return nil,nil,errors.New("RsaOaep.WrapNewKey(): expected key to be '*rsa.PublicKey'") +} + +func (alg *RsaOaep) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + if privKey,ok:=key.(*rsa.PrivateKey);ok { + return rsa.DecryptOAEP(alg.sha(), rand.Reader, privKey, encryptedCek, nil) + } + + return nil,errors.New("RsaOaep.Unwrap(): expected key to be '*rsa.PrivateKey'") +} + +func (alg *RsaOaep) sha() hash.Hash { + switch alg.shaSizeBits { + case 1: return sha1.New() + default: return sha256.New() + } +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/rsa_pkcs1v15.go b/vendor/github.com/dvsekhvalnov/jose2go/rsa_pkcs1v15.go new file mode 100644 index 0000000000..10dac0664f --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/rsa_pkcs1v15.go @@ -0,0 +1,41 @@ +package jose + +import ( + "errors" + "crypto/rsa" + "crypto/rand" + "github.com/dvsekhvalnov/jose2go/arrays" +) + +func init() { + RegisterJwa(new(RsaPkcs1v15)) +} + +// RS-AES using PKCS #1 v1.5 padding key management algorithm implementation +type RsaPkcs1v15 struct{ +} + +func (alg *RsaPkcs1v15) Name() string { + return RSA1_5 +} + +func (alg *RsaPkcs1v15) WrapNewKey(cekSizeBits int, key interface{}, header map[string]interface{}) (cek []byte, encryptedCek []byte, err error) { + if pubKey,ok:=key.(*rsa.PublicKey);ok { + if cek,err = arrays.Random(cekSizeBits>>3);err==nil { + encryptedCek,err=rsa.EncryptPKCS1v15(rand.Reader,pubKey,cek) + return + } + + return nil,nil,err + } + + return nil,nil,errors.New("RsaPkcs1v15.WrapNewKey(): expected key to be '*rsa.PublicKey'") +} + +func (alg *RsaPkcs1v15) Unwrap(encryptedCek []byte, key interface{}, cekSizeBits int, header map[string]interface{}) (cek []byte, err error) { + if privKey,ok:=key.(*rsa.PrivateKey);ok { + return rsa.DecryptPKCS1v15(rand.Reader,privKey,encryptedCek) + } + + return nil,errors.New("RsaPkcs1v15.Unwrap(): expected key to be '*rsa.PrivateKey'") +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/rsa_using_sha.go b/vendor/github.com/dvsekhvalnov/jose2go/rsa_using_sha.go new file mode 100644 index 0000000000..647a45a4ee --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/rsa_using_sha.go @@ -0,0 +1,50 @@ +package jose + +import ( + "crypto/rand" + "crypto/rsa" + "errors" +) + +func init() { + RegisterJws(&RsaUsingSha{keySizeBits: 256}) + RegisterJws(&RsaUsingSha{keySizeBits: 384}) + RegisterJws(&RsaUsingSha{keySizeBits: 512}) +} + +// RSA using SHA signature algorithm implementation +type RsaUsingSha struct{ + keySizeBits int +} + +func (alg *RsaUsingSha) Name() string { + switch alg.keySizeBits { + case 256: return RS256 + case 384: return RS384 + default: return RS512 + } +} + +func (alg *RsaUsingSha) Verify(securedInput, signature []byte, key interface{}) error { + + if pubKey,ok:=key.(*rsa.PublicKey);ok { + return rsa.VerifyPKCS1v15(pubKey, hashFunc(alg.keySizeBits), sha(alg.keySizeBits, securedInput), signature) + } + + return errors.New("RsaUsingSha.Verify(): expects key to be '*rsa.PublicKey'") +} + +func (alg *RsaUsingSha) Sign(securedInput []byte, key interface{}) (signature []byte, err error) { + + if privKey,ok:=key.(*rsa.PrivateKey);ok { + return rsa.SignPKCS1v15(rand.Reader, privKey, hashFunc(alg.keySizeBits), sha(alg.keySizeBits, securedInput)) + } + + return nil,errors.New("RsaUsingSha.Sign(): expects key to be '*rsa.PrivateKey'") +} + +func sha(keySizeBits int, input []byte) (hash []byte) { + hasher := hashAlg(keySizeBits) + hasher.Write(input) + return hasher.Sum(nil) +} \ No newline at end of file diff --git a/vendor/github.com/dvsekhvalnov/jose2go/rsapss_using_sha.go b/vendor/github.com/dvsekhvalnov/jose2go/rsapss_using_sha.go new file mode 100644 index 0000000000..fc111db23d --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/rsapss_using_sha.go @@ -0,0 +1,43 @@ +package jose + +import ( + "crypto/rand" + "crypto/rsa" + "errors" +) + +func init() { + RegisterJws(&RsaPssUsingSha{keySizeBits: 256, saltSizeBytes: 32}) + RegisterJws(&RsaPssUsingSha{keySizeBits: 384, saltSizeBytes: 48}) + RegisterJws(&RsaPssUsingSha{keySizeBits: 512, saltSizeBytes: 64}) +} + +// RSA with PSS using SHA signing algorithm implementation +type RsaPssUsingSha struct{ + keySizeBits int + saltSizeBytes int +} + +func (alg *RsaPssUsingSha) Name() string { + switch alg.keySizeBits { + case 256: return PS256 + case 384: return PS384 + default: return PS512 + } +} + +func (alg *RsaPssUsingSha) Verify(securedInput, signature []byte, key interface{}) error { + if pubKey,ok:=key.(*rsa.PublicKey);ok { + return rsa.VerifyPSS(pubKey, hashFunc(alg.keySizeBits), sha(alg.keySizeBits, securedInput), signature, &rsa.PSSOptions{SaltLength:alg.saltSizeBytes}) + } + + return errors.New("RsaPssUsingSha.Verify(): expects key to be '*rsa.PublicKey'") +} + +func (alg *RsaPssUsingSha) Sign(securedInput []byte, key interface{}) (signature []byte, err error) { + if privKey,ok:=key.(*rsa.PrivateKey);ok { + return rsa.SignPSS(rand.Reader, privKey, hashFunc(alg.keySizeBits), sha(alg.keySizeBits, securedInput), &rsa.PSSOptions{SaltLength:alg.saltSizeBytes}) + } + + return nil,errors.New("RsaPssUsingSha.Sign(): expects key to be '*rsa.PrivateKey'") +} diff --git a/vendor/github.com/dvsekhvalnov/jose2go/sha.go b/vendor/github.com/dvsekhvalnov/jose2go/sha.go new file mode 100644 index 0000000000..13b7b17ef4 --- /dev/null +++ b/vendor/github.com/dvsekhvalnov/jose2go/sha.go @@ -0,0 +1,24 @@ +package jose + +import ( + "hash" + "crypto" + "crypto/sha256" + "crypto/sha512" +) + +func hashFunc(keySizeBits int) crypto.Hash { + switch keySizeBits { + case 256: return crypto.SHA256 + case 384: return crypto.SHA384 + default: return crypto.SHA512 + } +} + +func hashAlg(keySizeBits int) hash.Hash { + switch keySizeBits { + case 256: return sha256.New() + case 384: return sha512.New384() + default: return sha512.New() + } +} \ No newline at end of file diff --git a/vendor/github.com/godbus/dbus/.travis.yml b/vendor/github.com/godbus/dbus/.travis.yml new file mode 100644 index 0000000000..752dc0a02e --- /dev/null +++ b/vendor/github.com/godbus/dbus/.travis.yml @@ -0,0 +1,46 @@ +dist: xenial +language: go +go_import_path: github.com/godbus/dbus +sudo: true + +go: + - 1.7.3 + - 1.8.7 + - 1.9.5 + - 1.10.1 + - tip + +env: + global: + matrix: + - TARGET=amd64 + - TARGET=arm64 + - TARGET=arm + - TARGET=386 + - TARGET=ppc64le + +matrix: + fast_finish: true + allow_failures: + - go: tip + exclude: + - go: tip + env: TARGET=arm + - go: tip + env: TARGET=arm64 + - go: tip + env: TARGET=386 + - go: tip + env: TARGET=ppc64le + +addons: + apt: + packages: + - dbus + - dbus-x11 + +before_install: + +script: + - go test -v -race ./... # Run all the tests with the race detector enabled + - go vet ./... # go vet is the official Go static analyzer diff --git a/vendor/github.com/godbus/dbus/CONTRIBUTING.md b/vendor/github.com/godbus/dbus/CONTRIBUTING.md new file mode 100644 index 0000000000..c88f9b2bdd --- /dev/null +++ b/vendor/github.com/godbus/dbus/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# How to Contribute + +## Getting Started + +- Fork the repository on GitHub +- Read the [README](README.markdown) for build and test instructions +- Play with the project, submit bugs, submit patches! + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +- Create a topic branch from where you want to base your work (usually master). +- Make commits of logical units. +- Make sure your commit messages are in the proper format (see below). +- Push your changes to a topic branch in your fork of the repository. +- Make sure the tests pass, and add any new tests as appropriate. +- Submit a pull request to the original repository. + +Thanks for your contributions! + +### Format of the Commit Message + +We follow a rough convention for commit messages that is designed to answer two +questions: what changed and why. The subject line should feature the what and +the body of the commit should describe the why. + +``` +scripts: add the test-cluster command + +this uses tmux to setup a test cluster that you can easily kill and +start for debugging. + +Fixes #38 +``` + +The format can be described more formally as follows: + +``` +: + + + +