Skip to content

Commit

Permalink
Add helper for encoding/decoding root tokens and OTP generation in SD…
Browse files Browse the repository at this point in the history
…K module (#10504) (#10505)
  • Loading branch information
ptzianos authored Dec 1, 2021
1 parent 6625418 commit 14aded6
Show file tree
Hide file tree
Showing 18 changed files with 239 additions and 90 deletions.
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

0 comments on commit 14aded6

Please sign in to comment.