Migrate sdk/recovery to api/ and sdk/wrapper to internal (#1507)
* `recovery` is only used by the API (and the main module, for decryption) * `wrapper` is only used by the CLI right now Moving these brings them closer to the code that uses them, and makes the SDK package a bit more clear in terms of functionality.
This commit is contained in:
1 parent
3fc2cf4df5
commit
081de3cf79
16 files changed
+17
-683
No files matched your search
+1
-1
@@ -19,7 +19,7 @@ import (
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/hashicorp/boundary/sdk/recovery"
|
||||
"github.com/hashicorp/boundary/api/recovery"
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
retryablehttp "github.com/hashicorp/go-retryablehttp"
|
||||
|
||||
+3
-1
@@ -6,13 +6,15 @@ replace github.com/hashicorp/boundary/sdk => ../sdk
|
||||
|
||||
require (
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/hashicorp/boundary/sdk v0.0.8
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
github.com/hashicorp/go-kms-wrapping v0.6.5
|
||||
github.com/hashicorp/go-retryablehttp v0.6.8
|
||||
github.com/hashicorp/go-rootcerts v1.0.2
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1
|
||||
github.com/hashicorp/go-uuid v1.0.2
|
||||
github.com/mr-tron/base58 v1.2.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
|
||||
google.golang.org/grpc v1.35.0
|
||||
google.golang.org/protobuf v1.27.1
|
||||
)
|
||||
+1
-14
@@ -45,9 +45,6 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
|
||||
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
@@ -210,14 +207,10 @@ github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR3
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.2/go.mod h1:QRJZ7siKie+SZJB9jLbfKrs0Gd0yPWMtbneg0iU1PrY=
|
||||
github.com/hashicorp/go-secure-stdlib/configutil v0.1.2/go.mod h1:EN1DJMjv9y5e/uRAuP2WPibAox5KHEiq2BjZl00aNPQ=
|
||||
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.1/go.mod h1:irE9ILwhE8VooWdhHhBy0qXzOMRlHKoWoMlr31mAFCE=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1 h1:78ki3QBevHwYrVxnyVeaEz+7WtifHhauYF23es/0KlI=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
|
||||
github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1/go.mod h1:Ch/bf00Qnx77MZd49JRgHYqHQjtEmTgGU2faufpVZb0=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
|
||||
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
|
||||
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
@@ -235,12 +228,9 @@ github.com/hashicorp/vault/sdk v0.1.14-0.20200519221530-14615acda45f/go.mod h1:W
|
||||
github.com/hashicorp/vault/sdk v0.2.1/go.mod h1:WfUiO1vYzfBkz1TmoE4ZGU7HD0T0Cl/rZwaxjBkgN4U=
|
||||
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/huaweicloud/golangsdk v0.0.0-20200304081349-45ec0797f2a4/go.mod h1:WQBcHRNX9shz3928lWEvstQJtAtYI7ks6XlgtRT9Tcw=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f/go.mod h1:3J2qVK16Lq8V+wfiL2lPeDZ7UWMxk5LemerHa1p6N00=
|
||||
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
@@ -273,7 +263,6 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
@@ -374,9 +363,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@@ -607,7 +595,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/mr-tron/base58"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
nonceLength = 32
|
||||
v1String = "1"
|
||||
)
|
||||
|
||||
// FutureLeeway indicates how far in the future we should allow the creation
|
||||
// time of the token to be, in order to account for clock drift
|
||||
var FutureLeeway = time.Minute
|
||||
|
||||
// Info is the struct required to be marshaled to be used as a token
|
||||
// for the recovery workflow.
|
||||
type Info struct {
|
||||
Nonce string `json:"nonce"`
|
||||
NonceBytes []byte `json:"-"`
|
||||
CreationTime time.Time `json:"creation_time"`
|
||||
}
|
||||
|
||||
func GenerateRecoveryToken(ctx context.Context, wrapper wrapping.Wrapper) (string, error) {
|
||||
b, err := uuid.GenerateRandomBytes(nonceLength)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error generating random bytes for recovery token nonce: %w", err)
|
||||
}
|
||||
info := &Info{
|
||||
Nonce: base64.RawStdEncoding.EncodeToString(b),
|
||||
CreationTime: time.Now(),
|
||||
}
|
||||
|
||||
return formatToken(ctx, wrapper, info)
|
||||
}
|
||||
|
||||
func formatToken(ctx context.Context, wrapper wrapping.Wrapper, info *Info) (string, error) {
|
||||
marshaledInfo, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error marshaling recovery info: %w", err)
|
||||
}
|
||||
|
||||
blobInfo, err := wrapper.Encrypt(ctx, marshaledInfo, nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error encrypting recovery info: %w", err)
|
||||
}
|
||||
|
||||
marshaledBlob, err := proto.Marshal(blobInfo)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error marshaling encrypted blob: %w", err)
|
||||
}
|
||||
|
||||
encodedMarshaledBlob := base58.FastBase58Encoding(marshaledBlob)
|
||||
|
||||
return fmt.Sprintf("r_%s%s", v1String, encodedMarshaledBlob), nil
|
||||
}
|
||||
|
||||
func ParseRecoveryToken(ctx context.Context, wrapper wrapping.Wrapper, versionedToken string) (*Info, error) {
|
||||
versionedToken = strings.TrimSpace(versionedToken)
|
||||
if versionedToken == "" {
|
||||
return nil, errors.New("empty token")
|
||||
}
|
||||
if !strings.HasPrefix(versionedToken, "r_") {
|
||||
return nil, errors.New("token has wrong format")
|
||||
}
|
||||
versionedToken = strings.TrimPrefix(versionedToken, "r_")
|
||||
|
||||
ver, token := versionedToken[0:1], versionedToken[1:]
|
||||
|
||||
// Don't do anything yet, but if we ever need to adjust data based on
|
||||
// version we'd do it here
|
||||
switch ver {
|
||||
case v1String:
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown recovery token version %s", ver)
|
||||
}
|
||||
|
||||
marshaledBlob, err := base58.FastBase58Decoding(token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error base58-decoding token: %w", err)
|
||||
}
|
||||
if len(marshaledBlob) == 0 {
|
||||
return nil, fmt.Errorf("length zero after base58-decoding token")
|
||||
}
|
||||
|
||||
blobInfo := new(wrapping.EncryptedBlobInfo)
|
||||
if err := proto.Unmarshal(marshaledBlob, blobInfo); err != nil {
|
||||
return nil, fmt.Errorf("error decoding encrypted blob: %w", err)
|
||||
}
|
||||
|
||||
marshaledInfo, err := wrapper.Decrypt(ctx, blobInfo, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decrypting recovery info: %w", err)
|
||||
}
|
||||
|
||||
info := new(Info)
|
||||
if err := json.Unmarshal(marshaledInfo, info); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling recovery info: %w", err)
|
||||
}
|
||||
|
||||
info.NonceBytes, err = base64.RawStdEncoding.DecodeString(info.Nonce)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error decoding nonce bytes: %w", err)
|
||||
}
|
||||
if len(info.NonceBytes) != nonceLength {
|
||||
return nil, errors.New("nonce has incorrect length, must be 32 bytes")
|
||||
}
|
||||
|
||||
if info.CreationTime.IsZero() {
|
||||
return nil, errors.New("recovery token creation time is zero")
|
||||
}
|
||||
// It must be before the current time. This means someone can't create
|
||||
// one way in the future and keep using it. We fudge this by 1 minute to
|
||||
// account for time discrepancies between systems.
|
||||
if info.CreationTime.After(time.Now().Add(FutureLeeway)) {
|
||||
return nil, errors.New("recovery token creation time is invalid")
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping"
|
||||
"github.com/hashicorp/go-kms-wrapping/wrappers/aead"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func testWrapper(t *testing.T) wrapping.Wrapper {
|
||||
rootKey := make([]byte, 32)
|
||||
n, err := rand.Read(rootKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n != 32 {
|
||||
t.Fatal(n)
|
||||
}
|
||||
root := aead.NewWrapper(nil)
|
||||
_, err = root.SetConfig(map[string]string{
|
||||
"key_id": base64.StdEncoding.EncodeToString(rootKey),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := root.SetAESGCMKeyBytes(rootKey); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
func TestRecoveryTokens(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
wrapper := testWrapper(t)
|
||||
b, err := uuid.GenerateRandomBytes(nonceLength)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, b, 32)
|
||||
b64Nonce := base64.RawStdEncoding.EncodeToString(b)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
withTime time.Time
|
||||
withNonce string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "normal",
|
||||
},
|
||||
{
|
||||
name: "zero time",
|
||||
withNonce: b64Nonce,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "future time",
|
||||
withNonce: b64Nonce,
|
||||
withTime: time.Now().Add(5 * time.Minute),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty nonce",
|
||||
withNonce: string(""),
|
||||
withTime: time.Now(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong nonce length",
|
||||
withNonce: base64.RawStdEncoding.EncodeToString(append(b, b...)),
|
||||
withTime: time.Now(),
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "bad nonce format",
|
||||
withNonce: string(b),
|
||||
withTime: time.Now(),
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
if tt.withTime.IsZero() && tt.withNonce == "" {
|
||||
token, err := GenerateRecoveryToken(ctx, wrapper)
|
||||
require.NoError(err)
|
||||
assert.NotEmpty(token)
|
||||
info, err := ParseRecoveryToken(ctx, wrapper, token)
|
||||
require.NoError(err)
|
||||
assert.NotNil(info)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := formatToken(ctx, wrapper, &Info{
|
||||
Nonce: tt.withNonce,
|
||||
CreationTime: tt.withTime,
|
||||
})
|
||||
require.NoError(err)
|
||||
assert.NotEmpty(token)
|
||||
info, err := ParseRecoveryToken(ctx, wrapper, token)
|
||||
if tt.wantErr {
|
||||
require.Error(err)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.NotNil(info)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user