Initial import
This commit is contained in:
69
aws.go
Normal file
69
aws.go
Normal 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
76
decryption.go
Normal 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
152
decryption_test.go
Normal 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
8
doc.go
Normal 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
93
env.go
Normal 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
60
env_test.go
Normal 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
47
go.mod
Normal 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
115
go.sum
Normal 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
5
keyring.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package secret
|
||||
|
||||
func Keyring(service string) (Provider, error) {
|
||||
return keyringProvider(service)
|
||||
}
|
22
keyring_darwin.go
Normal file
22
keyring_darwin.go
Normal 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
40
keyring_darwin_test.go
Normal 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
20
keyring_stub.go
Normal 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
177
keyring_unix.go
Normal 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
104
keyring_windows.go
Normal 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
57
provider.go
Normal 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
65
provider_test.go
Normal 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
4
testdata/env
vendored
Normal 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
89
vault.go
Normal 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
25
vault_test.go
Normal 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),
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user