diff --git a/api/go.mod b/api/go.mod index d2fd451cb..49ba032d0 100644 --- a/api/go.mod +++ b/api/go.mod @@ -9,7 +9,7 @@ require ( github.com/frankban/quicktest v1.13.0 // indirect github.com/go-test/deep v1.0.2 github.com/hashicorp/errwrap v1.1.0 - github.com/hashicorp/go-cleanhttp v0.5.1 + github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-hclog v0.16.2 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/go-retryablehttp v0.6.6 diff --git a/api/go.sum b/api/go.sum index d5e55d2f5..61498240d 100644 --- a/api/go.sum +++ b/api/go.sum @@ -89,8 +89,9 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= @@ -138,6 +139,7 @@ github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyX github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/changelog/10505.txt b/changelog/10505.txt new file mode 100644 index 000000000..3c52855b1 --- /dev/null +++ b/changelog/10505.txt @@ -0,0 +1,3 @@ +```release-note:improvement +sdk: Add helper for decoding root tokens +``` \ No newline at end of file diff --git a/command/operator_generate_root.go b/command/operator_generate_root.go index 1d5450267..ece541683 100644 --- a/command/operator_generate_root.go +++ b/command/operator_generate_root.go @@ -2,19 +2,15 @@ package command import ( "bytes" - "crypto/rand" - "encoding/base64" "fmt" "io" "os" "strings" - "github.com/hashicorp/go-secure-stdlib/base62" "github.com/hashicorp/go-secure-stdlib/password" - uuid "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/helper/pgpkeys" - "github.com/hashicorp/vault/helper/xor" + "github.com/hashicorp/vault/sdk/helper/roottoken" "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -290,32 +286,15 @@ func (c *OperatorGenerateRootCommand) generateOTP(client *api.Client, kind gener return "", 2 } - switch status.OTPLength { - case 0: - // This is the fallback case - buf := make([]byte, 16) - readLen, err := rand.Read(buf) - if err != nil { - c.UI.Error(fmt.Sprintf("Error reading random bytes: %s", err)) - return "", 2 - } - - if readLen != 16 { - c.UI.Error(fmt.Sprintf("Read %d bytes when we should have read 16", readLen)) - return "", 2 - } - - return base64.StdEncoding.EncodeToString(buf), 0 - - default: - otp, err := base62.Random(status.OTPLength) - if err != nil { - c.UI.Error(fmt.Errorf("Error reading random bytes: %w", err).Error()) - return "", 2 - } - - return otp, 0 + otp, err := roottoken.GenerateOTP(status.OTPLength) + var retCode int + if err != nil { + retCode = 2 + c.UI.Error(err.Error()) + } else { + retCode = 0 } + return otp, retCode } // decode decodes the given value using the otp. @@ -364,36 +343,10 @@ func (c *OperatorGenerateRootCommand) decode(client *api.Client, encoded, otp st return 2 } - var token string - switch status.OTPLength { - case 0: - // Backwards compat - tokenBytes, err := xor.XORBase64(encoded, otp) - if err != nil { - c.UI.Error(fmt.Sprintf("Error xoring token: %s", err)) - return 1 - } - - uuidToken, err := uuid.FormatUUID(tokenBytes) - if err != nil { - c.UI.Error(fmt.Sprintf("Error formatting base64 token value: %s", err)) - return 1 - } - token = strings.TrimSpace(uuidToken) - - default: - tokenBytes, err := base64.RawStdEncoding.DecodeString(encoded) - if err != nil { - c.UI.Error(fmt.Errorf("Error decoding base64'd token: %w", err).Error()) - return 1 - } - - tokenBytes, err = xor.XORBytes(tokenBytes, []byte(otp)) - if err != nil { - c.UI.Error(fmt.Errorf("Error xoring token: %w", err).Error()) - return 1 - } - token = string(tokenBytes) + token, err := roottoken.DecodeToken(encoded, otp, status.OTPLength) + if err != nil { + c.UI.Error(fmt.Sprintf("Error decoding root token: %s", err)) + return 1 } switch Format(c.UI) { diff --git a/command/operator_generate_root_test.go b/command/operator_generate_root_test.go index 84ebc3f94..f4e0c9453 100644 --- a/command/operator_generate_root_test.go +++ b/command/operator_generate_root_test.go @@ -11,7 +11,7 @@ import ( "strings" "testing" - "github.com/hashicorp/vault/helper/xor" + "github.com/hashicorp/vault/sdk/helper/xor" "github.com/hashicorp/vault/vault" "github.com/mitchellh/cli" ) diff --git a/helper/testhelpers/testhelpers.go b/helper/testhelpers/testhelpers.go index 84cb407e4..bca97f671 100644 --- a/helper/testhelpers/testhelpers.go +++ b/helper/testhelpers/testhelpers.go @@ -17,8 +17,8 @@ import ( "github.com/hashicorp/vault/api" "github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/helper/namespace" - "github.com/hashicorp/vault/helper/xor" "github.com/hashicorp/vault/physical/raft" + "github.com/hashicorp/vault/sdk/helper/xor" "github.com/hashicorp/vault/vault" "github.com/mitchellh/go-testing-interface" ) diff --git a/http/sys_generate_root_test.go b/http/sys_generate_root_test.go index d77659a87..60e951436 100644 --- a/http/sys_generate_root_test.go +++ b/http/sys_generate_root_test.go @@ -14,7 +14,7 @@ import ( "github.com/go-test/deep" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/helper/pgpkeys" - "github.com/hashicorp/vault/helper/xor" + "github.com/hashicorp/vault/sdk/helper/xor" "github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/vault" ) diff --git a/sdk/go.mod b/sdk/go.mod index 0aa17297c..045ebd9e7 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -13,6 +13,7 @@ require ( github.com/golang/protobuf v1.5.2 github.com/golang/snappy v0.0.4 github.com/hashicorp/errwrap v1.1.0 + github.com/hashicorp/go-cleanhttp v0.5.2 github.com/hashicorp/go-hclog v0.16.2 github.com/hashicorp/go-immutable-radix v1.3.1 github.com/hashicorp/go-kms-wrapping/entropy v0.1.0 @@ -29,6 +30,7 @@ require ( github.com/hashicorp/go-version v1.2.0 github.com/hashicorp/golang-lru v0.5.4 github.com/hashicorp/hcl v1.0.0 + github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.6 // indirect github.com/mitchellh/copystructure v1.0.0 diff --git a/sdk/go.sum b/sdk/go.sum index 396026093..f78df89eb 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -89,6 +89,8 @@ github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brv github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.2 h1:K4ev2ib4LdQETX5cSZBG0DVLk1jwGqSPXBjdah3veNs= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= @@ -135,6 +137,8 @@ github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyX github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f h1:Gsc9mVHLRqBjMgdQCghN9NObCcRncDqxJvBvEaIIQEo= +github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f/go.mod h1:ghbZscTyKdM07+Fw3KSi0hcJm+AlEUWj8QLlPtijN/M= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= diff --git a/sdk/helper/roottoken/decode.go b/sdk/helper/roottoken/decode.go new file mode 100644 index 000000000..cc9300690 --- /dev/null +++ b/sdk/helper/roottoken/decode.go @@ -0,0 +1,40 @@ +package roottoken + +import ( + "encoding/base64" + "fmt" + "strings" + + uuid "github.com/hashicorp/go-uuid" + "github.com/hashicorp/vault/sdk/helper/xor" +) + +// DecodeToken will decode the root token returned by the Vault API +// The algorithm was initially used in the generate root command +func DecodeToken(encoded, otp string, otpLength int) (string, error) { + switch otpLength { + case 0: + // Backwards compat + tokenBytes, err := xor.XORBase64(encoded, otp) + if err != nil { + return "", fmt.Errorf("error xoring token: %s", err) + } + + uuidToken, err := uuid.FormatUUID(tokenBytes) + if err != nil { + return "", fmt.Errorf("error formatting base64 token value: %s", err) + } + return strings.TrimSpace(uuidToken), nil + default: + tokenBytes, err := base64.RawStdEncoding.DecodeString(encoded) + if err != nil { + return "", fmt.Errorf("error decoding base64'd token: %v", err) + } + + tokenBytes, err = xor.XORBytes(tokenBytes, []byte(otp)) + if err != nil { + return "", fmt.Errorf("error xoring token: %v", err) + } + return string(tokenBytes), nil + } +} diff --git a/sdk/helper/roottoken/encode.go b/sdk/helper/roottoken/encode.go new file mode 100644 index 000000000..2537d9397 --- /dev/null +++ b/sdk/helper/roottoken/encode.go @@ -0,0 +1,26 @@ +package roottoken + +import ( + "encoding/base64" + "fmt" + + "github.com/hashicorp/vault/sdk/helper/xor" +) + +// EncodeToken gets a token and an OTP and encodes the token. +// The OTP must have the same length as the token. +func EncodeToken(token, otp string) (string, error) { + if len(token) == 0 { + return "", fmt.Errorf("no token provided") + } else if len(otp) == 0 { + return "", fmt.Errorf("no otp provided") + } + + // This function performs decoding checks so rather than decode the OTP, + // just encode the value we're passing in. + tokenBytes, err := xor.XORBytes([]byte(otp), []byte(token)) + if err != nil { + return "", fmt.Errorf("xor of root token failed: %w", err) + } + return base64.RawStdEncoding.EncodeToString(tokenBytes), nil +} diff --git a/sdk/helper/roottoken/encode_test.go b/sdk/helper/roottoken/encode_test.go new file mode 100644 index 000000000..9df26928e --- /dev/null +++ b/sdk/helper/roottoken/encode_test.go @@ -0,0 +1,72 @@ +package roottoken + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestTokenEncodingDecodingWithOTP(t *testing.T) { + otpTestCases := []struct { + token string + name string + otpLength int + expectedEncodingErr string + expectedDecodingErr string + }{ + { + token: "someToken", + name: "test token encoding with base64", + otpLength: 0, + expectedEncodingErr: "xor of root token failed: length of byte slices is not equivalent: 24 != 9", + expectedDecodingErr: "", + }, + { + token: "someToken", + name: "test token encoding with base62", + otpLength: len("someToken"), + expectedEncodingErr: "", + expectedDecodingErr: "", + }, + { + token: "someToken", + name: "test token encoding with base62 - wrong otp length", + otpLength: len("someToken") + 1, + expectedEncodingErr: "xor of root token failed: length of byte slices is not equivalent: 10 != 9", + expectedDecodingErr: "", + }, + { + token: "", + name: "test no token to encode", + otpLength: 0, + expectedEncodingErr: "no token provided", + expectedDecodingErr: "", + }, + } + for _, otpTestCase := range otpTestCases { + t.Run(otpTestCase.name, func(t *testing.T) { + otp, err := GenerateOTP(otpTestCase.otpLength) + if err != nil { + t.Fatal(err.Error()) + } + encodedToken, err := EncodeToken(otpTestCase.token, otp) + if err != nil || otpTestCase.expectedDecodingErr != "" { + assert.EqualError(t, err, otpTestCase.expectedEncodingErr) + return + } + assert.NotEqual(t, otp, encodedToken) + assert.NotEqual(t, encodedToken, otpTestCase.token) + decodedToken, err := DecodeToken(encodedToken, otp, len(otp)) + if err != nil || otpTestCase.expectedDecodingErr != "" { + assert.EqualError(t, err, otpTestCase.expectedDecodingErr) + return + } + assert.Equal(t, otpTestCase.token, decodedToken) + }) + } +} + +func TestTokenEncodingDecodingWithNoOTPorPGPKey(t *testing.T) { + _, err := EncodeToken("", "") + assert.EqualError(t, err, "no token provided") +} diff --git a/sdk/helper/roottoken/otp.go b/sdk/helper/roottoken/otp.go new file mode 100644 index 000000000..5a12c4f0a --- /dev/null +++ b/sdk/helper/roottoken/otp.go @@ -0,0 +1,40 @@ +package roottoken + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + + "github.com/hashicorp/go-secure-stdlib/base62" +) + +// DefaultBase64EncodedOTPLength is the number of characters that will be randomly generated +// before the Base64 encoding process takes place. +const defaultBase64EncodedOTPLength = 16 + +// GenerateOTP generates a random token and encodes it as a Base64 or as a Base62 encoded string. +// Returns 0 if the generation completed without any error, 2 otherwise, along with the error. +func GenerateOTP(otpLength int) (string, error) { + switch otpLength { + case 0: + // This is the fallback case + buf := make([]byte, defaultBase64EncodedOTPLength) + readLen, err := rand.Read(buf) + if err != nil { + return "", fmt.Errorf("error reading random bytes: %s", err) + } + + if readLen != defaultBase64EncodedOTPLength { + return "", fmt.Errorf("read %d bytes when we should have read 16", readLen) + } + + return base64.StdEncoding.EncodeToString(buf), nil + default: + otp, err := base62.Random(otpLength) + if err != nil { + return "", fmt.Errorf("error reading random bytes: %w", err) + } + + return otp, nil + } +} diff --git a/sdk/helper/roottoken/otp_test.go b/sdk/helper/roottoken/otp_test.go new file mode 100644 index 000000000..437e8f3d0 --- /dev/null +++ b/sdk/helper/roottoken/otp_test.go @@ -0,0 +1,19 @@ +package roottoken + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBase64OTPGeneration(t *testing.T) { + token, err := GenerateOTP(0) + assert.Len(t, token, 24) + assert.Nil(t, err) +} + +func TestBase62OTPGeneration(t *testing.T) { + token, err := GenerateOTP(20) + assert.Len(t, token, 20) + assert.Nil(t, err) +} diff --git a/helper/xor/xor.go b/sdk/helper/xor/xor.go similarity index 100% rename from helper/xor/xor.go rename to sdk/helper/xor/xor.go diff --git a/helper/xor/xor_test.go b/sdk/helper/xor/xor_test.go similarity index 100% rename from helper/xor/xor_test.go rename to sdk/helper/xor/xor_test.go diff --git a/vault/generate_root.go b/vault/generate_root.go index b701e5bfe..39c4bb6bb 100644 --- a/vault/generate_root.go +++ b/vault/generate_root.go @@ -9,8 +9,8 @@ import ( "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/helper/pgpkeys" - "github.com/hashicorp/vault/helper/xor" "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/hashicorp/vault/sdk/helper/roottoken" "github.com/hashicorp/vault/shamir" ) @@ -318,40 +318,28 @@ func (c *Core) GenerateRootUpdate(ctx context.Context, key []byte, nonce string, return nil, err } - var tokenBytes []byte + var encodedToken string - // Get the encoded value first so that if there is an error we don't create - // the root token. switch { case len(c.generateRootConfig.OTP) > 0: - // This function performs decoding checks so rather than decode the OTP, - // just encode the value we're passing in. - tokenBytes, err = xor.XORBytes([]byte(c.generateRootConfig.OTP), []byte(token)) - if err != nil { - cleanupFunc() - c.logger.Error("xor of root token failed", "error", err) - return nil, err - } - token = base64.RawStdEncoding.EncodeToString(tokenBytes) - + encodedToken, err = roottoken.EncodeToken(token, c.generateRootConfig.OTP) case len(c.generateRootConfig.PGPKey) > 0: - _, tokenBytesArr, err := pgpkeys.EncryptShares([][]byte{[]byte(token)}, []string{c.generateRootConfig.PGPKey}) - if err != nil { - cleanupFunc() - c.logger.Error("error encrypting new root token", "error", err) - return nil, err - } - token = base64.StdEncoding.EncodeToString(tokenBytesArr[0]) - + var tokenBytesArr [][]byte + _, tokenBytesArr, err = pgpkeys.EncryptShares([][]byte{[]byte(token)}, []string{c.generateRootConfig.PGPKey}) + encodedToken = base64.StdEncoding.EncodeToString(tokenBytesArr[0]) default: + err = fmt.Errorf("unreachable condition") + } + + if err != nil { cleanupFunc() - return nil, fmt.Errorf("unreachable condition") + return nil, err } results := &GenerateRootResult{ Progress: progress, Required: config.SecretThreshold, - EncodedToken: token, + EncodedToken: encodedToken, PGPFingerprint: c.generateRootConfig.PGPFingerprint, } diff --git a/vault/generate_root_test.go b/vault/generate_root_test.go index f83fd1246..9401be7cb 100644 --- a/vault/generate_root_test.go +++ b/vault/generate_root_test.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/go-secure-stdlib/base62" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/helper/pgpkeys" - "github.com/hashicorp/vault/helper/xor" + "github.com/hashicorp/vault/sdk/helper/xor" ) func TestCore_GenerateRoot_Lifecycle(t *testing.T) {