Initial import

This commit is contained in:
2025-09-04 14:14:02 +02:00
commit ac609a54c2
19 changed files with 1228 additions and 0 deletions

69
aws.go Normal file
View File

@@ -0,0 +1,69 @@
package secret
import (
"context"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/kms"
"github.com/aws/aws-sdk-go-v2/service/ssm"
)
type awskms struct {
service *kms.Client
}
// AWSKeyManagement uses AWS KMS for decrypting blobs.
//
// The keys passed in GetSecret are the encrypted blobs and will be converted with [ToBinary].
func AWSKeyManagement(options ...func(*config.LoadOptions) error) (Provider, error) {
config, err := config.LoadDefaultConfig(context.TODO(), options...)
if err != nil {
return nil, err
}
return awskms{kms.NewFromConfig(config)}, nil
}
func (p awskms) GetSecret(key string) (value []byte, err error) {
input := new(kms.DecryptInput)
input.CiphertextBlob = ToBinary(key)
var output *kms.DecryptOutput
if output, err = p.service.Decrypt(context.TODO(), input); err != nil {
return
}
return output.Plaintext, nil
}
type awsps struct {
service *ssm.Client
}
// AWSParameterStorage uses AWS Session Manager Parameter Storage for obtaining secrets.
func AWSParameterStorage(options ...func(*config.LoadOptions) error) (Provider, error) {
config, err := config.LoadDefaultConfig(context.TODO(), options...)
if err != nil {
return nil, err
}
return awsps{service: ssm.NewFromConfig(config)}, nil
}
func (p awsps) GetSecret(key string) (value []byte, err error) {
var yesPlease = true
input := new(ssm.GetParameterInput)
input.Name = &key
input.WithDecryption = &yesPlease
var output *ssm.GetParameterOutput
if output, err = p.service.GetParameter(context.TODO(), input); err != nil {
return
}
if output.Parameter == nil {
return nil, NotFound{Key: key}
}
return []byte(*output.Parameter.Value), nil
}

76
decryption.go Normal file
View File

@@ -0,0 +1,76 @@
package secret
import (
"crypto/aes"
"crypto/cipher"
"errors"
"golang.org/x/crypto/chacha20poly1305"
)
type aead struct {
Provider
aead cipher.AEAD
}
// WithChaCha20Poly1305 wraps the returned value and decrypts it using ChaCha20-Poly1305 AEAD.
//
// The used nonce size is 12 bytes.
func WithChaCha20Poly1305(p Provider, key []byte) (Provider, error) {
cipher, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
return aead{
Provider: p,
aead: cipher,
}, nil
}
// WithChaCha20Poly1305X wraps the returned value and decrypts it using ChaCha20-Poly1305 AEAD.
//
// The used nonce size is 24 bytes.
func WithChaCha20Poly1305X(p Provider, key []byte) (Provider, error) {
cipher, err := chacha20poly1305.NewX(key)
if err != nil {
return nil, err
}
return aead{
Provider: p,
aead: cipher,
}, nil
}
// WithAESGCM wraps the returned value and decrypts it using AES GCM AEAD.
//
// The accepted key sizes are 16 bytes for AES-128 and 32 bytes for AES-256.
func WithAESGCM(p Provider, key []byte) (Provider, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
return aead{
Provider: p,
aead: gcm,
}, nil
}
func (p aead) GetSecret(key string) (value []byte, err error) {
if value, err = p.Provider.GetSecret(key); err != nil {
return
}
nonceSize := p.aead.NonceSize()
if len(value) < nonceSize {
return nil, errors.New("secret: ciphertext is too short")
}
nonce, ciphertext := value[:nonceSize], value[nonceSize:]
return p.aead.Open(nil, nonce, ciphertext, nil)
}

152
decryption_test.go Normal file
View File

@@ -0,0 +1,152 @@
package secret
import (
"bytes"
"encoding/hex"
"testing"
)
func TestWithAES128GCM(t *testing.T) {
key := []byte{
0x37, 0xe4, 0x62, 0x59, 0xa6, 0xef, 0xe9, 0x96,
0xb8, 0x5d, 0x2c, 0x35, 0xb5, 0x33, 0x3e, 0xff,
}
env := environment{"test": []byte{
0x1c, 0x74, 0x37, 0xe9, 0x9e, 0x37, 0xb8, 0x9e,
0xf0, 0x21, 0xc0, 0xec, 0xad, 0x9d, 0xdf, 0x67,
0x75, 0xfd, 0x00, 0x48, 0x20, 0x46, 0x2e, 0x14,
0xfb, 0x9d, 0x17, 0x9a, 0xe7, 0x9d, 0x6f, 0x39,
0x19, 0xd5, 0x3f, 0x22, 0xa1, 0xac, 0xdb, 0xff,
0x2c, 0x1f, 0x57, 0x0e, 0xbb, 0x5d, 0xff, 0x89,
0x62, 0x55, 0x2b, 0x3b, 0x5e, 0xb5, 0x67, 0xb4,
0x32, 0x92, 0x68, 0xcc, 0x55, 0x8e, 0xd3, 0xc7,
0xce,
}}
p, err := WithAESGCM(env, key)
if err != nil {
t.Fatal(err)
return
}
msg := []byte("Gophers, gophers, gophers everywhere!")
v, err := p.GetSecret("test")
if err != nil {
t.Fatal(err)
return
}
if !bytes.Equal(v, msg) {
t.Errorf("expected:\n%s\n\ngot:\n%s", hex.Dump(msg), hex.Dump(v))
}
}
func TestWithAES256GCM(t *testing.T) {
key := []byte{
0x50, 0x0a, 0x33, 0xf2, 0xe5, 0x85, 0xc1, 0xd3,
0x65, 0x6d, 0x32, 0xbe, 0xea, 0x7b, 0xce, 0x7e,
0x12, 0xbf, 0x7a, 0x47, 0x4d, 0x21, 0x09, 0xa0,
0x3e, 0x3f, 0x65, 0xc7, 0xae, 0x94, 0x6c, 0xe3,
}
env := environment{"test": []byte{
0x62, 0x97, 0x6b, 0xc1, 0x78, 0xef, 0x41, 0xa0,
0xd4, 0xdc, 0x05, 0x05, 0x66, 0xf6, 0x5f, 0x62,
0xca, 0x91, 0xae, 0xd7, 0x7c, 0xff, 0xad, 0xc1,
0xbf, 0xd5, 0x61, 0xe3, 0x09, 0x8d, 0x9c, 0xce,
0xac, 0x88, 0x55, 0x8c, 0x02, 0xd4, 0x30, 0xc9,
0x42, 0x38, 0xdf, 0xf9, 0x8a, 0x4c, 0x92, 0x34,
0xc7, 0x82, 0x24, 0xb4, 0x9e, 0x9e, 0xdc, 0x10,
0x96, 0x60, 0xa2, 0x92, 0x2a, 0x94, 0x9c, 0x3a,
0xd8,
}}
p, err := WithAESGCM(env, key)
if err != nil {
t.Fatal(err)
return
}
msg := []byte("Gophers, gophers, gophers everywhere!")
v, err := p.GetSecret("test")
if err != nil {
t.Fatal(err)
return
}
if !bytes.Equal(v, msg) {
t.Errorf("expected:\n%s\n\ngot:\n%s", hex.Dump(msg), hex.Dump(v))
}
}
func TestWithChaCha20Poly1305(t *testing.T) {
key := []byte{
0x01, 0x94, 0x99, 0xd6, 0x89, 0x82, 0x7f, 0xa1,
0x27, 0x82, 0x36, 0x11, 0x78, 0x22, 0xe5, 0x16,
0x21, 0xe2, 0xc6, 0x29, 0x0c, 0x14, 0x6c, 0xb8,
0x12, 0x46, 0x2f, 0xae, 0x9e, 0xa7, 0x17, 0x09,
}
env := environment{"test": []byte{
0x76, 0x7c, 0x7d, 0x5d, 0x36, 0xd1, 0x27, 0xca,
0x0b, 0x64, 0x28, 0x82, 0xc3, 0x3a, 0xd3, 0x24,
0x30, 0x77, 0x8f, 0xef, 0xf1, 0x3a, 0x9f, 0xa7,
0x41, 0x49, 0x62, 0x79, 0x9f, 0x65, 0x12, 0x4f,
0x29, 0xe6, 0x04, 0x83, 0xab, 0x5c, 0xbc, 0x0b,
0xe2, 0x82, 0xcc, 0x3c, 0x47, 0x7d, 0xaa, 0x6d,
0x4c, 0x71, 0x6b, 0x24, 0x01, 0xd0, 0xf9, 0x88,
0xcf, 0x88, 0xbe, 0xee, 0xe2, 0x77, 0x07, 0x18,
0xf1,
}}
p, err := WithChaCha20Poly1305(env, key)
if err != nil {
t.Fatal(err)
return
}
msg := []byte("Gophers, gophers, gophers everywhere!")
v, err := p.GetSecret("test")
if err != nil {
t.Fatal(err)
return
}
if !bytes.Equal(v, msg) {
t.Errorf("expected:\n%s\n\ngot:\n%s", hex.Dump(msg), hex.Dump(v))
}
}
func TestWithChaCha20Poly1305X(t *testing.T) {
key := []byte{
0x25, 0x20, 0xda, 0x7c, 0x97, 0x60, 0x20, 0xf5,
0x09, 0xa7, 0x42, 0x31, 0x08, 0x50, 0x76, 0xf6,
0x79, 0xc5, 0x38, 0x5b, 0xfb, 0xf4, 0x98, 0x56,
0xa1, 0x92, 0xd1, 0xa0, 0x08, 0x3e, 0xf5, 0xfd,
}
env := environment{"test": []byte{
0x59, 0xaf, 0xc4, 0x65, 0x60, 0x3f, 0x02, 0xd3,
0x87, 0xd3, 0x15, 0xac, 0xf3, 0x6b, 0x4a, 0x4f,
0xe8, 0x40, 0xe5, 0x4d, 0x80, 0x30, 0x7b, 0x3d,
0x8a, 0xf4, 0x18, 0x96, 0x89, 0x05, 0x4e, 0x31,
0x44, 0x8c, 0xb2, 0x84, 0x9d, 0x25, 0xce, 0x3b,
0xb4, 0x66, 0x36, 0x0f, 0xd2, 0xad, 0xb3, 0x78,
0xf8, 0x02, 0x1e, 0x6c, 0xf9, 0x6c, 0x1f, 0x71,
0x3c, 0x2b, 0x59, 0x6f, 0xc7, 0x92, 0x3b, 0x40,
0x89, 0x13, 0x93, 0x6b, 0xa0, 0x35, 0x4e, 0x6f,
0xd8, 0x31, 0x67, 0xee, 0xa2,
}}
p, err := WithChaCha20Poly1305X(env, key)
if err != nil {
t.Fatal(err)
return
}
msg := []byte("Gophers, gophers, gophers everywhere!")
v, err := p.GetSecret("test")
if err != nil {
t.Fatal(err)
return
}
if !bytes.Equal(v, msg) {
t.Errorf("expected:\n%s\n\ngot:\n%s", hex.Dump(msg), hex.Dump(v))
}
}

8
doc.go Normal file
View File

@@ -0,0 +1,8 @@
// Package secret implements secret storage providers.
//
// # Providers
//
// Providers of secrets accept a string as key and return a byte slice as result
// for the secret. It is up to the consumers of this package how to interpret
// the bytes. In most cases, the byte slice can be converted to a string.
package secret

93
env.go Normal file
View File

@@ -0,0 +1,93 @@
package secret
import (
"bufio"
"io"
"os"
"strings"
"syscall"
)
type environment map[string][]byte
// Environment provides environment variables as secrets.
func Environment() Provider {
return EnvironmentPrefix("")
}
// EnvironmentPrefix provides environment variables with a prefix as secrets.
//
// The prefix is stripped from the final secret key name.
func EnvironmentPrefix(prefix string) Provider {
env := make(environment)
for _, line := range syscall.Environ() {
if !strings.HasPrefix(line, prefix) {
continue
}
kv := strings.SplitN(line[len(prefix):], "=", 2)
if len(kv) == 2 {
env[kv[0]] = []byte(strings.TrimSpace(kv[1]))
}
}
return env
}
// EnvironmentFile reads a file containing key-value pairs.
//
// Line starting with `#` or `//` are ignored.
//
// Example:
//
// key=value
// # This comment is ignored
func EnvironmentFile(name string) (Provider, error) {
f, err := os.Open(name)
if err != nil {
return nil, err
}
defer func() { _ = f.Close() }()
return EnvironmentReader(f)
}
// EnvironmentReader reads key-value pairs separated by newlines.
//
// Line starting with `#` or `//` are ignored.
//
// Example:
//
// key=value
// # This comment is ignored
func EnvironmentReader(r io.Reader) (Provider, error) {
s := bufio.NewScanner(r)
s.Split(bufio.ScanLines)
env := make(environment)
for s.Scan() {
if err := s.Err(); err != nil {
if err == io.EOF {
break
}
return nil, err
}
line := strings.TrimSpace(s.Text())
if strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") {
continue
}
kv := strings.SplitN(line, "=", 2)
if len(kv) == 2 {
env[strings.TrimSpace(kv[0])] = []byte(strings.TrimSpace(kv[1]))
}
}
return env, nil
}
func (env environment) GetSecret(key string) (value []byte, err error) {
var ok bool
if value, ok = env[key]; ok {
return
}
return nil, NotFound{key}
}

60
env_test.go Normal file
View File

@@ -0,0 +1,60 @@
package secret
import (
"bytes"
"path/filepath"
"testing"
)
func TestEnvironment(t *testing.T) {
testProvider(t, Environment(),
testProviderCase{
Key: "USER",
Test: testNotEmpty,
})
}
func TestEnvironmentPrefix(t *testing.T) {
testProvider(t, EnvironmentPrefix("US"),
testProviderCase{
Key: "ER",
Test: testNotEmpty,
})
}
var testEnvironmentFileCases = []testProviderCase{
{
Key: "test",
Test: testEqualString("case"),
},
{
Key: "spaces",
Test: testEqualString("yeah"),
},
{
Key: "ignored",
Err: NotFound{"ignored"},
},
}
func TestEnvironmentFile(t *testing.T) {
p, err := EnvironmentFile(filepath.Join("testdata", "env"))
if err != nil {
t.Fatal(err)
return
}
testProvider(t, p, testEnvironmentFileCases...)
}
func TestEnvironmentReader(t *testing.T) {
p, err := EnvironmentReader(bytes.NewBufferString(`
#ignored=true
test=case
spaces = yeah
`))
if err != nil {
t.Fatal(err)
return
}
testProvider(t, p, testEnvironmentFileCases...)
}

47
go.mod Normal file
View File

@@ -0,0 +1,47 @@
module git.maze.io/go/secret
go 1.24.0
require (
github.com/aws/aws-sdk-go-v2/config v1.31.6
github.com/aws/aws-sdk-go-v2/service/ssm v1.64.2
github.com/hashicorp/vault/api v1.20.0
golang.org/x/crypto v0.41.0
)
require (
github.com/aws/aws-sdk-go-v2 v1.38.3 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/kms v1.45.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
github.com/keybase/go-keychain v0.0.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
golang.org/x/net v0.42.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect
)

115
go.sum Normal file
View File

@@ -0,0 +1,115 @@
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
github.com/aws/aws-sdk-go-v2/service/kms v1.45.1 h1:NhkI4kfcZYmcIM34a+q9drh3aMG1BthkyziOr7sRTv4=
github.com/aws/aws-sdk-go-v2/service/kms v1.45.1/go.mod h1:elyXIFqx79eHvd0cRAzYDYHajeoJEygkBjJto4HJddc=
github.com/aws/aws-sdk-go-v2/service/ssm v1.64.2 h1:6P4W42RUTZixRG6TgfRB8KlsqNzHtvBhs6sTbkVPZvk=
github.com/aws/aws-sdk-go-v2/service/ssm v1.64.2/go.mod h1:wtxdacy3oO5sHO03uOtk8HMGfgo1gBHKwuJdYM220i0=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
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=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
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/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
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.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
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/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
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/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
github.com/hashicorp/vault/api v1.20.0 h1:KQMHElgudOsr+IbJgmbjHnCTxEpKs9LnozA1D3nozU4=
github.com/hashicorp/vault/api v1.20.0/go.mod h1:GZ4pcjfzoOWpkJ3ijHNpEoAxKEsBJnVljyTe3jM2Sms=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

5
keyring.go Normal file
View File

@@ -0,0 +1,5 @@
package secret
func Keyring(service string) (Provider, error) {
return keyringProvider(service)
}

22
keyring_darwin.go Normal file
View File

@@ -0,0 +1,22 @@
package secret
import "github.com/keybase/go-keychain"
type keyring struct {
service string
}
func keyringProvider(service string) (Provider, error) {
return keyring{
service: service,
}, nil
}
func (p keyring) GetSecret(key string) (value []byte, err error) {
return keychain.GetGenericPassword(
p.service, // service
key, // account
"", // label
"", // accessgroup
)
}

40
keyring_darwin_test.go Normal file
View File

@@ -0,0 +1,40 @@
package secret
import (
"testing"
"github.com/keybase/go-keychain"
)
const (
testKeyringService = "io.maze.git.go.secret"
testKeyringKey = "test"
)
func TestKeyring(t *testing.T) {
item := keychain.NewGenericPassword(testKeyringService, testKeyringKey, "", []byte(testKeyringKey), "")
if err := keychain.AddItem(item); err != nil {
t.Skip(err)
}
defer func() {
if err := keychain.DeleteGenericPasswordItem(testKeyringService, testKeyringKey); err != nil {
t.Error(err)
}
}()
p, err := Keyring(testKeyringService)
if err != nil {
t.Fatal(err)
return
}
v, err := p.GetSecret(testKeyringKey)
if err != nil {
t.Fatal(err)
return
}
if string(v) != testKeyringKey {
t.Errorf("expected %q, got %q", testKeyringKey, v)
}
}

20
keyring_stub.go Normal file
View File

@@ -0,0 +1,20 @@
//go:build !darwin && !windows && !(dragonfly && cgo) && !(freebsd && cgo) && !linux && !netbsd && !openbsd
package secret
import (
"errors"
"runtime"
)
var errKeyringNotSupported = errors.New("secret: keyring is not supported on " + runtime.GOOS)
type keyring struct{}
func keyringProvider(_ string) (Provider, error) {
return nil, errKeyringNotSupported
}
func (keyring) GetSecret(_ string) (_ []byte, err error) {
return nil, errKeyringNotSupported
}

177
keyring_unix.go Normal file
View File

@@ -0,0 +1,177 @@
//go:build (dragonfly && cgo) || (freebsd && cgo) || linux || netbsd || openbsd
package secret
import (
"fmt"
"github.com/godbus/dbus/v5"
)
const (
dbusServiceName = "org.freedesktop.secrets"
dbusServicePath = "/org/freedesktop/secrets"
dbusServiceInterface = "org.freedesktop.Secret.Service"
dbusServiceClose = dbusServiceInterface + ".Close"
dbusServiceUnlock = dbusServiceInterface + ".Unlock"
dbusCollectionBasePath = "/org/freedesktop/secrets/collection/"
dbusCollectionLoginPath = dbusCollectionBasePath + "login"
dbusCollectionLoginAliasPath = "/org/freedesktop/secrets/aliases/default"
dbusCollectionInterface = "org.freedesktop.Secret.Collection"
dbusCollectionGetSecret = dbusCollectionInterface + ".GetSecret"
dbusCollectionSearchItems = dbusCollectionInterface + ".SearchItems"
dbusPromptInterface = "org.freedesktop.Secret.Prompt"
dbusPrompt = dbusPromptInterface + ".Prompt"
dbusPromptCompleted = dbusPromptInterface + ".Completed"
)
type keyring struct {
service string
session *dbus.Conn
object dbus.BusObject
}
type keyringSecret struct {
Session dbus.ObjectPath
Parameters []byte
Value []byte
ContentType string `dbus:"content_type"`
}
func keyringProvider(service string) (Provider, error) {
session, err := dbus.SessionBus()
if err != nil {
return nil, err
}
return &keyring{
service: service,
session: session,
object: session.Object(dbusServiceName, dbusServicePath),
}, nil
}
func (p *keyring) Close() error {
return p.object.Call(dbusServiceClose, 0).Err
}
func (p *keyring) GetSecret(key string) (value []byte, err error) {
collection := p.getLoginCollection()
if err = p.unlock(collection.Path()); err != nil {
return
}
query := map[string]string{
"service": p.service,
"username": key,
}
var results []dbus.ObjectPath
if results, err = p.search(collection, query); err != nil {
return
} else if len(results) == 0 {
return nil, NotFound{Key: key}
}
var secret *keyringSecret
if secret, err = p.getSecret(results[0]); err != nil {
return
}
return secret.Value, nil
}
func (p keyring) checkCollectionPath(here dbus.ObjectPath) bool {
obj := p.session.Object(dbusServiceName, dbusServicePath)
val, err := obj.GetProperty(dbusCollectionInterface)
if err != nil {
return false
}
paths := val.Value().([]dbus.ObjectPath)
for _, p := range paths {
if p == here {
return true
}
}
return false
}
func (p keyring) getLoginCollection() dbus.BusObject {
here := dbus.ObjectPath(dbusCollectionLoginPath)
if !p.checkCollectionPath(here) {
here = dbus.ObjectPath(dbusCollectionLoginAliasPath)
}
return p.session.Object(dbusServiceName, here)
}
func (p *keyring) search(collection dbus.BusObject, query interface{}) (results []dbus.ObjectPath, err error) {
err = collection.Call(dbusCollectionSearchItems, 0, query).Store(&results)
return
}
func (p *keyring) getSecret(here dbus.ObjectPath) (secret *keyringSecret, err error) {
secret = new(keyringSecret)
err = p.session.Object(dbusServiceName, here).Call(dbusCollectionGetSecret, 0).Store(secret)
return
}
func (p *keyring) unlock(here dbus.ObjectPath) (err error) {
var (
unlocked []dbus.ObjectPath
prompt dbus.ObjectPath
)
if err = p.object.Call(dbusServiceUnlock, 0, []dbus.ObjectPath{here}).Store(&unlocked, &prompt); err != nil {
return
}
var variant dbus.Variant
if _, variant, err = p.handlePrompt(prompt); err != nil {
return
}
collections := variant.Value()
switch t := collections.(type) {
case []dbus.ObjectPath:
unlocked = append(unlocked, t...)
}
if len(unlocked) != 1 || (here != dbusCollectionLoginAliasPath && unlocked[0] != here) {
return fmt.Errorf("secret: failed to unlock keyring collection %q", here)
}
return
}
func (s *keyring) handlePrompt(prompt dbus.ObjectPath) (bool, dbus.Variant, error) {
if prompt != dbus.ObjectPath("/") {
err := s.session.AddMatchSignal(dbus.WithMatchObjectPath(prompt),
dbus.WithMatchInterface(dbusPromptInterface),
)
if err != nil {
return false, dbus.MakeVariant(""), err
}
defer func(s *keyring, options ...dbus.MatchOption) {
_ = s.session.RemoveMatchSignal(options...)
}(s, dbus.WithMatchObjectPath(prompt), dbus.WithMatchInterface(dbusPromptInterface))
promptSignal := make(chan *dbus.Signal, 1)
s.session.Signal(promptSignal)
err = s.session.Object(dbusServiceName, prompt).Call(dbusPrompt, 0, "").Err
if err != nil {
return false, dbus.MakeVariant(""), err
}
signal := <-promptSignal
switch signal.Name {
case dbusPromptCompleted:
dismissed := signal.Body[0].(bool)
result := signal.Body[1].(dbus.Variant)
return dismissed, result, nil
}
}
return false, dbus.MakeVariant(""), nil
}

104
keyring_windows.go Normal file
View File

@@ -0,0 +1,104 @@
package secret
import (
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var (
modadvapi32 = windows.NewLazySystemDLL("advapi32.dll")
procCredRead = modadvapi32.NewProc("CredReadW")
procCredFree winproc = modadvapi32.NewProc("CredFree")
)
// Interface for syscall.Proc: helps testing
type winproc interface {
Call(a ...uintptr) (r1, r2 uintptr, lastErr error)
}
type keyring struct {
service string
}
func keyringProvider(service string) (Provider, error) {
return keyring{service}, nil
}
func (p keyring) GetSecret(key string) (value []byte, err error) {
return sysCredRead(p.service+":"+key, sysCRED_TYPE_GENERIC)
}
// 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-_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/api/wincred/nf-wincred-credreadw
func sysCredRead(targetName string, typ sysCRED_TYPE) ([]byte, 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 goBytes(pcred.CredentialBlob, pcred.CredentialBlobSize), nil
}
// 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(&struct {
Data unsafe.Pointer
Len int
Cap int
}{
Data: unsafe.Pointer(src),
Len: int(len),
Cap: int(len),
})))
/*
copy(rv, *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
Data: src,
Len: int(len),
Cap: int(len),
})))
*/
return rv
}

57
provider.go Normal file
View File

@@ -0,0 +1,57 @@
package secret
import (
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
)
// Provider for secrets.
type Provider interface {
// GetSecret loads a secret by named key.
GetSecret(key string) (value []byte, err error)
}
// AmbiguousKey is an error incdicating that the secret doesn't resolve to exactly one item.
type AmbiguousKey struct {
Key string
}
func (err AmbiguousKey) Error() string {
return fmt.Sprintf("secret: ambigious secret key %q", err.Key)
}
// NotFound is an error indicating the secret can not be found.
type NotFound struct {
Key string
}
func (err NotFound) Error() string {
if err.Key == "" {
return "secret: not found"
}
return fmt.Sprintf("secret: %q not found", err.Key)
}
// ToBinary converts a string to []bytes.
//
// There are special prefixes for binary encoded formats:
// - hex: for hexadecimal encoded strings
// - b64: for base-64 encoded strings (raw encoding)
//
// If a special prefix is found, the appropriate codec will be used to decode to string to []byte,
// when there is an error decoding, it may result in an empty value.
//
// All other strings will be converted to []byte as-is.
func ToBinary(s string) (bytes []byte) {
switch {
case strings.HasPrefix(s, "hex:"):
bytes, _ = hex.DecodeString(s[4:])
case strings.HasPrefix(s, "b64:"):
bytes, _ = base64.RawStdEncoding.DecodeString(s[4:])
default:
bytes = []byte(s)
}
return
}

65
provider_test.go Normal file
View File

@@ -0,0 +1,65 @@
package secret
import (
"bytes"
"errors"
"fmt"
"testing"
)
type testProviderCase struct {
Name string
Key string
Err error
Test func([]byte) error
}
func testProvider(t *testing.T, p Provider, tests ...testProviderCase) {
t.Helper()
for _, test := range tests {
if test.Name == "" {
test.Name = test.Key
}
t.Run(test.Name, func(it *testing.T) {
v, err := p.GetSecret(test.Key)
if err != nil {
if test.Err == nil {
it.Fatalf("unexpected error %q in %T (%#+v)", err, p, p)
return
} else if !errors.Is(err, test.Err) {
it.Fatalf("unexpected error %q, expected error %q", err, test.Err)
return
}
return
} else if test.Err != nil {
it.Fatalf("expected error %q", test.Err)
return
}
if err := test.Test(v); err != nil {
it.Error(err)
}
})
}
}
func testNotEmpty(v []byte) error {
if len(v) > 0 {
return nil
}
return fmt.Errorf("expected empty, got %q", v)
}
func testEqual(a []byte) func([]byte) error {
return func(b []byte) error {
if bytes.Equal(a, b) {
return nil
}
return fmt.Errorf("expected %q, got %q", a, b)
}
}
func testEqualString(a string) func([]byte) error {
return testEqual([]byte(a))
}

4
testdata/env vendored Normal file
View File

@@ -0,0 +1,4 @@
# This is a test case for the Environment provider
#ignored=true
test=case
spaces = yeah

89
vault.go Normal file
View File

@@ -0,0 +1,89 @@
package secret
import (
"fmt"
"strings"
vault "github.com/hashicorp/vault/api"
)
type vaultProvider struct{}
// Vault provider uses Hashicorp Vault for secrets storage.
//
// The secretKey is used to
func Vault() Provider {
return vaultProvider{}
}
func (p vaultProvider) GetSecret(key string) (value []byte, err error) {
var (
config = vault.DefaultConfig()
client *vault.Client
)
if client, err = vault.NewClient(config); err != nil {
return
}
var mounts map[string]*vault.MountOutput
if mounts, err = client.Sys().ListMounts(); err != nil {
return
}
var (
mount *vault.MountOutput
location string
)
if mount, location, err = p.split(key, mounts); err != nil {
return
}
switch mount.Type {
case "generic", "kv":
// Supported
default:
return nil, fmt.Errorf("secret: mount type %q is not supported", mount.Type)
}
var secret *vault.Secret
if secret, err = client.Logical().Read(location); err != nil {
return
} else if secret == nil || len(secret.Data) == 0 {
return nil, NotFound{Key: key}
}
data, ok := secret.Data["data"].(map[string]interface{})
if !ok {
return nil, NotFound{Key: key}
}
if len(data) == 1 {
for _, item := range data {
switch data := item.(type) {
case []byte:
return data, nil
case string:
return []byte(data), nil
default:
return nil, fmt.Errorf("secret: unexpected return type %T", data)
}
}
}
return nil, AmbiguousKey{Key: key}
}
func (p vaultProvider) split(key string, mounts map[string]*vault.MountOutput) (mount *vault.MountOutput, location string, err error) {
location = strings.TrimPrefix(key, "/")
var tree string
for tree, mount = range mounts {
if strings.HasPrefix(location, tree) {
if mount.Type == "kv" && mount.Options["version"] == "2" {
location = tree + "data/" + location[len(tree):]
}
return
}
}
return nil, "", fmt.Errorf("secret: no Vault mount found for secret path %q", key)
}

25
vault_test.go Normal file
View File

@@ -0,0 +1,25 @@
package secret
import (
"os"
"testing"
)
func TestVault(t *testing.T) {
testKey := os.Getenv("TEST_VAULT_KEY")
if testKey == "" {
t.Skip("TEST_VAULT_KEY not set, which should contain the path to a secret for testing")
return
}
testValue := os.Getenv("TEST_VAULT_VALUE")
if testValue == "" {
t.Skip("TEST_VAULT_VALUE not set, which should contain the secret value for testing")
return
}
p := Vault()
testProvider(t, p, testProviderCase{
Key: testKey,
Test: testEqualString(testValue),
})
}