Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add helper for encoding/decoding root tokens and OTP generation in SDK module (#10504) #10505

Merged
merged 17 commits into from
Dec 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion api/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
3 changes: 3 additions & 0 deletions changelog/10505.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
sdk: Add helper for decoding root tokens
```
73 changes: 13 additions & 60 deletions command/operator_generate_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion command/operator_generate_root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
2 changes: 1 addition & 1 deletion helper/testhelpers/testhelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
2 changes: 1 addition & 1 deletion http/sys_generate_root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
2 changes: 2 additions & 0 deletions sdk/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions sdk/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
40 changes: 40 additions & 0 deletions sdk/helper/roottoken/decode.go
Original file line number Diff line number Diff line change
@@ -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
}
}
26 changes: 26 additions & 0 deletions sdk/helper/roottoken/encode.go
Original file line number Diff line number Diff line change
@@ -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
}
72 changes: 72 additions & 0 deletions sdk/helper/roottoken/encode_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
40 changes: 40 additions & 0 deletions sdk/helper/roottoken/otp.go
Original file line number Diff line number Diff line change
@@ -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
}
}
19 changes: 19 additions & 0 deletions sdk/helper/roottoken/otp_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
File renamed without changes.
File renamed without changes.
Loading