From 118ec9b2edd5b196f7aa65ce4bca49f18bf6c420 Mon Sep 17 00:00:00 2001 From: Sven Rebhan Date: Wed, 6 Oct 2021 11:56:49 +0200 Subject: [PATCH 1/4] Add keyctl backend implementation. --- config.go | 6 + go.mod | 6 +- go.sum | 13 +- keyctl.go | 328 +++++++++++++++++++++++++++++++++++++++++++++++++ keyctl_test.go | 253 ++++++++++++++++++++++++++++++++++++++ keyring.go | 5 + 6 files changed, 604 insertions(+), 7 deletions(-) create mode 100644 keyctl.go create mode 100644 keyctl_test.go diff --git a/config.go b/config.go index 1c5d0f7..191eb93 100644 --- a/config.go +++ b/config.go @@ -29,6 +29,12 @@ type Config struct { // FileDir is the directory that keyring files are stored in, ~ is resolved to home dir FileDir string + // KeyCtlScope is the scope of the kernel keyring (either "user", "session", "process" or "thread") + KeyCtlScope string + + // KeyCtlPerm is the permission mask to use for new keys + KeyCtlPerm uint32 + // KWalletAppID is the application id for KWallet KWalletAppID string diff --git a/go.mod b/go.mod index 4310878..b872f18 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,12 @@ require ( github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d github.com/kr/pretty v0.1.0 // indirect github.com/mtibben/percent v0.2.1 - github.com/stretchr/objx v0.2.0 // indirect + github.com/stretchr/objx v0.3.0 // indirect + github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 - golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 // indirect + golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) replace github.com/keybase/go-keychain => github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 diff --git a/go.sum b/go.sum index 0fcb1b7..a9fce08 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,6 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/danieljoos/wincred v1.0.2 h1:zf4bhty2iLuwgjgpraD2E9UbvO+fe54XXGJbOwe23fU= github.com/danieljoos/wincred v1.0.2/go.mod h1:SnuYRW9lp1oJrZX/dXJqr0cPK5gYXqx3EJbmjhLdK9U= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 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= @@ -21,12 +20,12 @@ github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= 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/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -36,5 +35,9 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI= golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/keyctl.go b/keyctl.go new file mode 100644 index 0000000..22ee4f0 --- /dev/null +++ b/keyctl.go @@ -0,0 +1,328 @@ +//go:build linux +// +build linux + +package keyring + +import ( + "errors" + "fmt" + "strings" + "syscall" + "unsafe" + + "golang.org/x/sys/unix" +) + +const ( + KEYCTL_PERM_VIEW = uint32(1 << 0) + KEYCTL_PERM_READ = uint32(1 << 1) + KEYCTL_PERM_WRITE = uint32(1 << 2) + KEYCTL_PERM_SEARCH = uint32(1 << 3) + KEYCTL_PERM_LINK = uint32(1 << 4) + KEYCTL_PERM_SETATTR = uint32(1 << 5) + KEYCTL_PERM_ALL = uint32((1 << 6) - 1) + + KEYCTL_PERM_OTHERS = 0 + KEYCTL_PERM_GROUP = 8 + KEYCTL_PERM_USER = 16 + KEYCTL_PERM_PROCESS = 24 +) + +// GetPermissions constructs the permission mask from the elements +func GetPermissions(process, user, group, others uint32) uint32 { + perm := others << KEYCTL_PERM_OTHERS + perm |= group << KEYCTL_PERM_GROUP + perm |= user << KEYCTL_PERM_USER + perm |= process << KEYCTL_PERM_PROCESS + + return perm +} + +// GetKeyringIDForScope get the keyring ID for a given scope +func GetKeyringIDForScope(scope string) (int32, error) { + ringRef, err := getKeyringForScope(scope) + if err != nil { + return 0, err + } + id, err := unix.KeyctlGetKeyringID(int(ringRef), false) + return int32(id), err +} + +type keyctlKeyring struct { + keyring int32 + perm uint32 +} + +func init() { + supportedBackends[KeyCtlBackend] = opener(func(cfg Config) (Keyring, error) { + keyring := keyctlKeyring{} + if cfg.KeyCtlPerm > 0 { + keyring.perm = cfg.KeyCtlPerm + } + + parent, err := getKeyringForScope(cfg.KeyCtlScope) + if err != nil { + return nil, fmt.Errorf("accessing %q keyring failed: %v", cfg.KeyCtlScope, err) + } + + // Check for named keyrings + keyring.keyring = parent + if cfg.ServiceName != "" { + namedKeyring, err := keyctl_search(parent, "keyring", cfg.ServiceName) + if err != nil { + if !errors.Is(err, syscall.ENOKEY) { + return nil, fmt.Errorf("opening named %q keyring failed: %v", cfg.KeyCtlScope, err) + } + + // Keyring does not yet exist, create it + namedKeyring, err = keyring.createNamedKeyring(parent, cfg.ServiceName) + if err != nil { + return nil, fmt.Errorf("creating named %q keyring failed: %v", cfg.KeyCtlScope, err) + } + } + keyring.keyring = namedKeyring + } + + return &keyring, nil + }) +} + +func (k *keyctlKeyring) Get(name string) (Item, error) { + key, err := keyctl_search(k.keyring, "user", name) + if err != nil { + if errors.Is(err, syscall.ENOKEY) { + return Item{}, ErrKeyNotFound + } + return Item{}, err + } + // data, err := key.Get() + data, err := keyctl_read(key) + if err != nil { + return Item{}, err + } + + item := Item{ + Key: name, + Data: data, + } + + return item, nil +} + +// GetMetadata for pass returns an error indicating that it's unsupported for this backend. +// TODO: We can deliver metadata different from the defined ones (e.g. permissions, expire-time, etc). +func (k *keyctlKeyring) GetMetadata(_ string) (Metadata, error) { + return Metadata{}, ErrMetadataNotSupported +} + +func (k *keyctlKeyring) Set(item Item) error { + if k.perm == 0 { + // Keep the default permissions (alswrv-----v------------) + _, err := keyctl_add(k.keyring, "user", item.Key, item.Data) + return err + } + + // By default we loose possession of the key in anything above the session keyring. + // Together with the default permissions (which cannot be changed during creation) we + // cannot change the permissions without possessing the key. Therefore, create the + // key in the session keyring, change permissions and then link to the target + // keyring and unlink from the intermediate keyring again. + key, err := keyctl_add(unix.KEY_SPEC_SESSION_KEYRING, "user", item.Key, item.Data) + if err != nil { + return fmt.Errorf("adding key to session failed: %v", err) + } + + if err := keyctl_setperm(key, k.perm); err != nil { + return fmt.Errorf("setting permission 0x%x failed: %v", k.perm, err) + } + + if err := keyctl_link(k.keyring, key); err != nil { + return fmt.Errorf("linking key to keyring failed: %v", err) + } + + if err := keyctl_unlink(unix.KEY_SPEC_SESSION_KEYRING, key); err != nil { + return fmt.Errorf("unlinking key from session failed: %v", err) + } + + return nil +} + +func (k *keyctlKeyring) Remove(name string) error { + key, err := keyctl_search(k.keyring, "user", name) + if err != nil { + return ErrKeyNotFound + } + + return keyctl_unlink(k.keyring, key) +} + +func (k *keyctlKeyring) Keys() ([]string, error) { + results := []string{} + + data, err := keyctl_read(k.keyring) + if err != nil { + return nil, fmt.Errorf("reading keyring failed: %v", err) + } + ids, err := keyctl_convertKeyBuffer(data) + if err != nil { + return nil, fmt.Errorf("converting raw keylist failed: %v", err) + } + + for _, id := range ids { + info, err := keyctl_describe(id) + if err != nil { + return nil, err + } + if info["type"] == "user" { + results = append(results, info["description"]) + } + } + + return results, nil +} + +// INTERNAL FUNCTIONS +func (k *keyctlKeyring) createNamedKeyring(parent int32, name string) (int32, error) { + if k.perm == 0 { + // Keep the default permissions (alswrv-----v------------) + return keyctl_add(parent, "keyring", name, nil) + } + + // By default we loose possession of the keyring in anything above the session keyring. + // Together with the default permissions (which cannot be changed during creation) we + // cannot change the permissions without possessing the keyring. Therefore, create the + // keyring linked to the session keyring, change permissions and then link to the target + // keyring and unlink from the intermediate keyring again. + keyring, err := keyctl_add(unix.KEY_SPEC_SESSION_KEYRING, "keyring", name, nil) + if err != nil { + return 0, fmt.Errorf("creating keyring failed: %v", err) + } + + if err := keyctl_setperm(keyring, k.perm); err != nil { + return 0, fmt.Errorf("setting permission 0x%x failed: %v", k.perm, err) + } + + if err := keyctl_link(k.keyring, keyring); err != nil { + return 0, fmt.Errorf("linking keyring failed: %v", err) + } + + if err := keyctl_unlink(unix.KEY_SPEC_SESSION_KEYRING, keyring); err != nil { + return 0, fmt.Errorf("unlinking keyring from session failed: %v", err) + } + + return keyring, nil +} + +func getKeyringForScope(scope string) (int32, error) { + switch scope { + case "user": + return int32(unix.KEY_SPEC_USER_KEYRING), nil + case "usersession": + return int32(unix.KEY_SPEC_USER_SESSION_KEYRING), nil + case "group": + // Not yet implemented in the kernel + // return int32(unix.KEY_SPEC_GROUP_KEYRING) + return 0, fmt.Errorf("scope %q not yet implemented", scope) + case "session": + return int32(unix.KEY_SPEC_SESSION_KEYRING), nil + case "process": + return int32(unix.KEY_SPEC_PROCESS_KEYRING), nil + case "thread": + return int32(unix.KEY_SPEC_THREAD_KEYRING), nil + } + return 0, fmt.Errorf("unknown scope %q", scope) +} + +// INTERNAL KEYCTL ABSTRACTION FUNCTIONS +func keyctl_add(parent int32, keytype, key string, data []byte) (int32, error) { + id, err := unix.AddKey(keytype, key, data, int(parent)) + if err != nil { + return 0, err + } + return int32(id), nil +} + +func keyctl_search(id int32, idtype, name string) (int32, error) { + key, err := unix.KeyctlSearch(int(id), idtype, name, 0) + if err != nil { + return 0, err + } + return int32(key), nil +} + +func keyctl_read(id int32) ([]byte, error) { + var buffer []byte + + for { + length, err := unix.KeyctlBuffer(unix.KEYCTL_READ, int(id), buffer, 0) + if err != nil { + return nil, err + } + + // Return the buffer if it was large enough + if length <= len(buffer) { + return buffer[:length], nil + } + + // Next try with a larger buffer + buffer = make([]byte, length) + } +} + +func keyctl_describe(id int32) (map[string]string, error) { + description, err := unix.KeyctlString(unix.KEYCTL_DESCRIBE, int(id)) + if err != nil { + return nil, err + } + fields := strings.Split(description, ";") + if len(fields) < 1 { + return nil, fmt.Errorf("no data") + } + + data := make(map[string]string) + names := []string{"type", "uid", "gid", "perm"} // according to keyctl_describe(3) new fields are added at the end + data["description"] = fields[len(fields)-1] // according to keyctl_describe(3) description is always last + for i, f := range fields[:len(fields)-1] { + if i >= len(names) { + // Do not stumble upon unknown fields + break + } + data[names[i]] = f + } + + return data, nil +} + +func keyctl_link(parent, child int32) error { + _, _, errno := syscall.Syscall(syscall.SYS_KEYCTL, uintptr(unix.KEYCTL_LINK), uintptr(child), uintptr(parent)) + if errno != 0 { + return errno + } + return nil +} + +func keyctl_unlink(parent, child int32) error { + _, _, errno := syscall.Syscall(syscall.SYS_KEYCTL, uintptr(unix.KEYCTL_UNLINK), uintptr(child), uintptr(parent)) + if errno != 0 { + return errno + } + return nil +} + +func keyctl_setperm(id int32, perm uint32) error { + return unix.KeyctlSetperm(int(id), perm) +} + +func keyctl_convertKeyBuffer(buffer []byte) ([]int32, error) { + if len(buffer)%4 != 0 { + return nil, fmt.Errorf("buffer size %d not a multiple of 4", len(buffer)) + } + + results := make([]int32, 0, len(buffer)/4) + for i := 0; i < len(buffer); i += 4 { + // We need to case in host-native endianess here as this is what we get from the kernel. + r := *((*int32)(unsafe.Pointer(&buffer[i]))) + results = append(results, int32(r)) + } + return results, nil +} diff --git a/keyctl_test.go b/keyctl_test.go new file mode 100644 index 0000000..4036204 --- /dev/null +++ b/keyctl_test.go @@ -0,0 +1,253 @@ +//go:build linux +// +build linux + +package keyring_test + +import ( + "errors" + "math/rand" + "syscall" + "testing" + "time" + + "github.com/99designs/keyring" + "golang.org/x/sys/unix" + + "github.com/stretchr/testify/require" +) + +var ringname = getRandomKeyringName(16) + +const ringparent = "thread" + +func getRandomKeyringName(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + rand.Seed(time.Now().UnixNano()) + + buf := make([]byte, length) + for i := range buf { + buf[i] = charset[rand.Intn(len(charset))] + } + return "keyctl_test_" + string(buf) +} + +func doesNamedKeyringExist() (bool, error) { + ringparentID, err := keyring.GetKeyringIDForScope(ringparent) + if err != nil { + return false, nil + } + + _, err = unix.KeyctlSearch(int(ringparentID), "keyring", ringname, 0) + if errors.Is(err, syscall.ENOKEY) { + return false, nil + } + return err == nil, err +} + +func cleanupNamedKeyring() { + ringparentID, err := keyring.GetKeyringIDForScope(ringparent) + if err != nil { + return + } + + named, err := unix.KeyctlSearch(int(ringparentID), "keyring", ringname, 0) + if err != nil { + return + } + _, _, err = syscall.Syscall(syscall.SYS_KEYCTL, uintptr(unix.KEYCTL_UNLINK), uintptr(named), uintptr(ringparentID)) +} + +func TestKeyCtlIsAvailable(t *testing.T) { + backends := keyring.AvailableBackends() + require.Containsf(t, backends, keyring.KeyCtlBackend, "keyctl backends not among %v", backends) +} + +func TestKeyCtlOpenFailWrongScope(t *testing.T) { + failingScopes := []string{"", "group", "invalid"} + for _, scope := range failingScopes { + _, err := keyring.Open(keyring.Config{ + AllowedBackends: []keyring.BackendType{keyring.KeyCtlBackend}, + KeyCtlScope: scope, + }) + require.Errorf(t, err, "scope %q should fail", scope) + } +} + +func TestKeyCtlOpen(t *testing.T) { + scopes := []string{"user", "session", "process", "thread"} + for _, scope := range scopes { + _, err := keyring.Open(keyring.Config{ + AllowedBackends: []keyring.BackendType{keyring.KeyCtlBackend}, + KeyCtlScope: scope, + }) + require.NoError(t, err) + } +} + +func TestKeyCtlOpenNamed(t *testing.T) { + exists, err := doesNamedKeyringExist() + require.Falsef(t, exists, "ring %q already exists in scope %q", ringname, ringparent) + require.NoErrorf(t, err, "checking for ring %q in scope %q failed: %v", ringname, ringparent, err) + t.Cleanup(cleanupNamedKeyring) + + _, err = keyring.Open(keyring.Config{ + AllowedBackends: []keyring.BackendType{keyring.KeyCtlBackend}, + KeyCtlScope: ringparent, + ServiceName: ringname, + }) + require.NoError(t, err) +} + +func TestKeyCtlSet(t *testing.T) { + kr, err := keyring.Open(keyring.Config{ + AllowedBackends: []keyring.BackendType{keyring.KeyCtlBackend}, + KeyCtlScope: "user", + KeyCtlPerm: 0x3f3f0000, // "alswrvalswrv------------" + }) + require.NoError(t, err) + + item1 := keyring.Item{ + Key: "test", + Data: []byte("loose lips sink ships"), + } + + require.NoError(t, kr.Set(item1)) + + item2, err := kr.Get("test") + require.NoError(t, err) + + require.Equal(t, item1, item2) + + require.NoError(t, kr.Remove("test")) + + _, err = kr.Get("test") + require.Error(t, err) + require.ErrorIs(t, err, keyring.ErrKeyNotFound) +} + +func TestKeyCtlSetNamed(t *testing.T) { + exists, err := doesNamedKeyringExist() + require.Falsef(t, exists, "ring %q already exists in scope %q", ringname, ringparent) + require.NoErrorf(t, err, "checking for ring %q in scope %q failed: %v", ringname, ringparent, err) + t.Cleanup(cleanupNamedKeyring) + + kr, err := keyring.Open(keyring.Config{ + AllowedBackends: []keyring.BackendType{keyring.KeyCtlBackend}, + KeyCtlScope: ringparent, + ServiceName: ringname, + KeyCtlPerm: 0x3f3f0000, // "alswrvalswrv------------" + }) + require.NoError(t, err) + + item1 := keyring.Item{ + Key: "test", + Data: []byte("loose lips sink ships"), + } + + require.NoError(t, kr.Set(item1)) + + item2, err := kr.Get("test") + require.NoError(t, err) + + require.Equal(t, item1, item2) + + require.NoError(t, kr.Remove("test")) + + _, err = kr.Get("test") + require.Error(t, err) + require.ErrorIs(t, err, keyring.ErrKeyNotFound) +} + +func TestKeyCtlList(t *testing.T) { + exists, err := doesNamedKeyringExist() + require.Falsef(t, exists, "ring %q already exists in scope %q", ringname, ringparent) + require.NoErrorf(t, err, "checking for ring %q in scope %q failed: %v", ringname, ringparent, err) + t.Cleanup(cleanupNamedKeyring) + + kr, err := keyring.Open(keyring.Config{ + AllowedBackends: []keyring.BackendType{keyring.KeyCtlBackend}, + KeyCtlScope: ringparent, + ServiceName: ringname, + KeyCtlPerm: 0x3f3f0000, // "alswrvalswrv------------" + }) + require.NoError(t, err) + + item1 := keyring.Item{ + Key: "test", + Data: []byte("loose lips sink ships"), + } + require.NoError(t, kr.Set(item1)) + + item2 := keyring.Item{ + Key: "foobar", + Data: []byte("don't foo the bar"), + } + require.NoError(t, kr.Set(item2)) + + keys, err := kr.Keys() + require.NoError(t, err) + + expected := []string{"test", "foobar"} + require.ElementsMatch(t, keys, expected) + + require.NoError(t, kr.Remove("test")) + require.NoError(t, kr.Remove("foobar")) +} + +func TestKeyCtlGetNonExisting(t *testing.T) { + exists, err := doesNamedKeyringExist() + require.Falsef(t, exists, "ring %q already exists in scope %q", ringname, ringparent) + require.NoErrorf(t, err, "checking for ring %q in scope %q failed: %v", ringname, ringparent, err) + t.Cleanup(cleanupNamedKeyring) + + kr, err := keyring.Open(keyring.Config{ + AllowedBackends: []keyring.BackendType{keyring.KeyCtlBackend}, + KeyCtlScope: ringparent, + ServiceName: ringname, + KeyCtlPerm: 0x3f3f0000, // "alswrvalswrv------------" + }) + require.NoError(t, err) + + _, err = kr.Get("llamas") + require.Error(t, err) + require.ErrorIs(t, err, keyring.ErrKeyNotFound) +} + +func TestKeyCtlRemoveNonExisting(t *testing.T) { + exists, err := doesNamedKeyringExist() + require.Falsef(t, exists, "ring %q already exists in scope %q", ringname, ringparent) + require.NoErrorf(t, err, "checking for ring %q in scope %q failed: %v", ringname, ringparent, err) + t.Cleanup(cleanupNamedKeyring) + + kr, err := keyring.Open(keyring.Config{ + AllowedBackends: []keyring.BackendType{keyring.KeyCtlBackend}, + KeyCtlScope: ringparent, + ServiceName: ringname, + KeyCtlPerm: 0x3f3f0000, // "alswrvalswrv------------" + }) + require.NoError(t, err) + + err = kr.Remove("no-such-key") + require.Error(t, err) + require.ErrorIs(t, err, keyring.ErrKeyNotFound) +} + +func TestKeyCtlListEmptyKeyring(t *testing.T) { + exists, err := doesNamedKeyringExist() + require.Falsef(t, exists, "ring %q already exists in scope %q", ringname, ringparent) + require.NoErrorf(t, err, "checking for ring %q in scope %q failed: %v", ringname, ringparent, err) + t.Cleanup(cleanupNamedKeyring) + + kr, err := keyring.Open(keyring.Config{ + AllowedBackends: []keyring.BackendType{keyring.KeyCtlBackend}, + KeyCtlScope: ringparent, + ServiceName: ringname, + KeyCtlPerm: 0x3f3f0000, // "alswrvalswrv------------" + }) + require.NoError(t, err) + + keys, err := kr.Keys() + require.NoError(t, err) + require.Len(t, keys, 0) +} diff --git a/keyring.go b/keyring.go index 5349b6f..7f892d7 100644 --- a/keyring.go +++ b/keyring.go @@ -17,6 +17,7 @@ const ( InvalidBackend BackendType = "" SecretServiceBackend BackendType = "secret-service" KeychainBackend BackendType = "keychain" + KeyCtlBackend BackendType = "keyctl" KWalletBackend BackendType = "kwallet" WinCredBackend BackendType = "wincred" FileBackend BackendType = "file" @@ -33,6 +34,7 @@ var backendOrder = []BackendType{ // Linux SecretServiceBackend, KWalletBackend, + KeyCtlBackend, // General PassBackend, FileBackend, @@ -119,6 +121,9 @@ var ErrKeyNotFound = errors.New("The specified item could not be found in the ke // backend which requires credentials even to see metadata. var ErrMetadataNeedsCredentials = errors.New("The keyring backend requires credentials for metadata access") +// ErrMetadataNotSupported is returned when Metadata is not available for the backend. +var ErrMetadataNotSupported = errors.New("The keyring backend does not support metadata access") + var ( // Debug specifies whether to print debugging output Debug bool From bd26e26660509d7eeabeacfca53dfe6abe18d2c9 Mon Sep 17 00:00:00 2001 From: Sven Rebhan Date: Wed, 6 Oct 2021 11:59:47 +0200 Subject: [PATCH 2/4] Use the explicit 'ErrMetadataNotSupported' error for wincred. --- wincred.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wincred.go b/wincred.go index 18ccbfa..d6f7a9c 100644 --- a/wincred.go +++ b/wincred.go @@ -57,7 +57,7 @@ func (k *windowsKeyring) Get(key string) (Item, error) { // for this backend. // TODO: This is a stub. Look into whether pass would support metadata in a usable way for keyring. func (k *windowsKeyring) GetMetadata(_ string) (Metadata, error) { - return Metadata{}, ErrMetadataNeedsCredentials + return Metadata{}, ErrMetadataNotSupported } func (k *windowsKeyring) Set(item Item) error { From f697ddc2c5c9677b992607603d66c4cf9ae9621f Mon Sep 17 00:00:00 2001 From: Sven Rebhan Date: Wed, 6 Oct 2021 12:00:33 +0200 Subject: [PATCH 3/4] Export prompt functions to be used by external users. --- file.go | 2 +- file_test.go | 4 ++-- keychain_test.go | 16 ++++++++-------- prompt.go | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/file.go b/file.go index 85e3f3d..fd2f6e5 100644 --- a/file.go +++ b/file.go @@ -67,7 +67,7 @@ func (k *fileKeyring) unlock() error { } if k.password == "" { - pwd, err := k.passwordFunc(fmt.Sprintf("Enter passphrase to unlock %s", dir)) + pwd, err := k.passwordFunc(fmt.Sprintf("Enter passphrase to unlock %q", dir)) if err != nil { return err } diff --git a/file_test.go b/file_test.go index fc98e3f..c9b411f 100644 --- a/file_test.go +++ b/file_test.go @@ -8,7 +8,7 @@ import ( func TestFileKeyringSetWhenEmpty(t *testing.T) { k := &fileKeyring{ dir: os.TempDir(), - passwordFunc: fixedStringPrompt("no more secrets"), + passwordFunc: FixedStringPrompt("no more secrets"), } item := Item{Key: "llamas", Data: []byte("llamas are great")} @@ -33,7 +33,7 @@ func TestFileKeyringSetWhenEmpty(t *testing.T) { func TestFileKeyringGetWithSlashes(t *testing.T) { k := &fileKeyring{ dir: os.TempDir(), - passwordFunc: fixedStringPrompt("no more secrets"), + passwordFunc: FixedStringPrompt("no more secrets"), } item := Item{Key: "https://aws-sso-portal.awsapps.com/start", Data: []byte("https://aws-sso-portal.awsapps.com/start")} diff --git a/keychain_test.go b/keychain_test.go index 7218382..965a72e 100644 --- a/keychain_test.go +++ b/keychain_test.go @@ -17,7 +17,7 @@ func TestOSXKeychainKeyringSet(t *testing.T) { k := &keychain{ path: path, - passwordFunc: fixedStringPrompt("test password"), + passwordFunc: FixedStringPrompt("test password"), service: "test", isTrusted: true, } @@ -57,7 +57,7 @@ func TestOSXKeychainKeyringOverwrite(t *testing.T) { k := &keychain{ path: path, - passwordFunc: fixedStringPrompt("test password"), + passwordFunc: FixedStringPrompt("test password"), service: "test", isTrusted: true, } @@ -110,7 +110,7 @@ func TestOSXKeychainKeyringListKeysWhenEmpty(t *testing.T) { k := &keychain{ path: path, service: "test", - passwordFunc: fixedStringPrompt("test password"), + passwordFunc: FixedStringPrompt("test password"), isTrusted: true, } @@ -130,7 +130,7 @@ func TestOSXKeychainKeyringListKeysWhenNotEmpty(t *testing.T) { k := &keychain{ path: path, service: "test", - passwordFunc: fixedStringPrompt("test password"), + passwordFunc: FixedStringPrompt("test password"), isTrusted: true, } @@ -175,7 +175,7 @@ func TestOSXKeychainGetKeyWhenEmpty(t *testing.T) { k := &keychain{ path: path, - passwordFunc: fixedStringPrompt("test password"), + passwordFunc: FixedStringPrompt("test password"), service: "test", isTrusted: true, } @@ -192,7 +192,7 @@ func TestOSXKeychainGetKeyWhenNotEmpty(t *testing.T) { k := &keychain{ path: path, - passwordFunc: fixedStringPrompt("test password"), + passwordFunc: FixedStringPrompt("test password"), service: "test", isTrusted: true, } @@ -222,7 +222,7 @@ func TestOSXKeychainRemoveKeyWhenEmpty(t *testing.T) { k := &keychain{ path: path, - passwordFunc: fixedStringPrompt("test password"), + passwordFunc: FixedStringPrompt("test password"), service: "test", isTrusted: true, } @@ -239,7 +239,7 @@ func TestOSXKeychainRemoveKeyWhenNotEmpty(t *testing.T) { k := &keychain{ path: path, - passwordFunc: fixedStringPrompt("test password"), + passwordFunc: FixedStringPrompt("test password"), service: "test", isTrusted: true, } diff --git a/prompt.go b/prompt.go index d5da409..feb0e7f 100644 --- a/prompt.go +++ b/prompt.go @@ -10,7 +10,7 @@ import ( // PromptFunc is a function used to prompt the user for a password type PromptFunc func(string) (string, error) -func terminalPrompt(prompt string) (string, error) { +func TerminalPrompt(prompt string) (string, error) { fmt.Printf("%s: ", prompt) b, err := terminal.ReadPassword(int(os.Stdin.Fd())) if err != nil { @@ -20,7 +20,7 @@ func terminalPrompt(prompt string) (string, error) { return string(b), nil } -func fixedStringPrompt(value string) PromptFunc { +func FixedStringPrompt(value string) PromptFunc { return func(_ string) (string, error) { return value, nil } From 4174f16d6706dd6a4328c57268b8715ea4e22fb0 Mon Sep 17 00:00:00 2001 From: Sven Rebhan Date: Wed, 6 Oct 2021 12:01:27 +0200 Subject: [PATCH 4/4] Run 'go fmt' with go1.17. --- keychain.go | 1 + keychain_test.go | 1 + kwallet.go | 1 + libsecret.go | 1 + libsecret_test.go | 1 + pass.go | 1 + pass_test.go | 1 + wincred.go | 1 + wincred_test.go | 1 + 9 files changed, 9 insertions(+) diff --git a/keychain.go b/keychain.go index 9caf54d..d5cf218 100644 --- a/keychain.go +++ b/keychain.go @@ -1,3 +1,4 @@ +//go:build darwin && cgo // +build darwin,cgo package keyring diff --git a/keychain_test.go b/keychain_test.go index 965a72e..887db7d 100644 --- a/keychain_test.go +++ b/keychain_test.go @@ -1,3 +1,4 @@ +//go:build darwin // +build darwin package keyring diff --git a/kwallet.go b/kwallet.go index 1e5a16e..42c9128 100644 --- a/kwallet.go +++ b/kwallet.go @@ -1,3 +1,4 @@ +//go:build linux // +build linux package keyring diff --git a/libsecret.go b/libsecret.go index ca4913d..779096e 100644 --- a/libsecret.go +++ b/libsecret.go @@ -1,3 +1,4 @@ +//go:build linux // +build linux package keyring diff --git a/libsecret_test.go b/libsecret_test.go index f165aec..56cebc3 100644 --- a/libsecret_test.go +++ b/libsecret_test.go @@ -1,3 +1,4 @@ +//go:build linux // +build linux package keyring diff --git a/pass.go b/pass.go index e6e2057..d8faeca 100644 --- a/pass.go +++ b/pass.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package keyring diff --git a/pass_test.go b/pass_test.go index 772cd01..1923bdb 100644 --- a/pass_test.go +++ b/pass_test.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package keyring diff --git a/wincred.go b/wincred.go index d6f7a9c..2ed43f2 100644 --- a/wincred.go +++ b/wincred.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package keyring diff --git a/wincred_test.go b/wincred_test.go index c93f6d5..52bf873 100644 --- a/wincred_test.go +++ b/wincred_test.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package keyring_test