zalando / go-keyring Goto Github PK
View Code? Open in Web Editor NEWCross-platform keyring interface for Go
License: MIT License
Cross-platform keyring interface for Go
License: MIT License
The example from the readme file throws an error on
keyring.Set(service, user, password)``:
No such interface 'org.freedesktop.Secret.Collection' on object at path /org/freedesktop/secrets/collection/login
Any idea why is that? If I run it with sudo the program just get stuck
package main
import (
"log"
"github.com/zalando/go-keyring"
)
func main() {
service := "my-app"
user := "anon"
password := "secret"
// set password
err := keyring.Set(service, user, password)
if err != nil {
log.Fatal(err)
}
// get password
secret, err := keyring.Get(service, user)
if err != nil {
log.Fatal(err)
}
log.Println(secret)
}
Requires a way to unlock/create the default keychain on the travis VM.
Issue appears with "/usr/bin/security -i". When invoking 'add-generic-password', the password value is limited to < 1024 bytes it seems.
Heya ๐
I was checking out gh (the github cli) and it got stuck after running gh auth login
. So I dug a little and localized the issue in the interaction between this library and keepassxc, which I use as a keyring.
As far as I can tell, both this library and keepassxc are weird with the secret service protocol, so I'll cross-post it to their issue tracker.
While playring around, I've encountered unhandled errors and freeze.
Not sure about the freezes, but the secret service api spec this regarding IsLocked:
A client application should always be ready to unlock the items for the secrets it needs, or objects it must modify. It must not assume that an item is already unlocked for whatever reason.
A service may lock previously unlocked items for any reason at any time. Usually this is done in response to user actions, timeouts, or external actions (such as the computer sleeping). The inherent race conditions present due to this are unavoidable, and must be handled gracefully.
gh auth login
freezes instead of exiting, but it does get a new token from github and saves it. It doesn't seem to apply, but as far as I can tell, that's because it fails to read the secret from the store. (Which arguably should be reported to the user, but it should also work.)
# 1.241 is gh
method call time=1681678963.125907 sender=:1.241 -> destination=org.freedesktop.secrets serial=2 path=/org/freedesktop/secrets; interface=org.freedesktop.DBus.Properties; member=Get
string "org.freedesktop.Secret.Service"
string "Collections"
method return time=1681678963.130388 sender=:1.28 -> destination=:1.241 serial=1575 reply_serial=2
variant array [
object path "/org/freedesktop/secrets/collection/passwords"
]
method call time=1681678963.130985 sender=:1.241 -> destination=org.freedesktop.secrets serial=3 path=/org/freedesktop/secrets; interface=org.freedesktop.Secret.Service; member=Unlock
array [
object path "/org/freedesktop/secrets/aliases/default"
]
method return time=1681678963.131601 sender=:1.28 -> destination=:1.241 serial=1576 reply_serial=3
array [
object path "/org/freedesktop/secrets/collection/passwords"
]
object path "/"
method call time=1681678963.132177 sender=:1.241 -> destination=org.freedesktop.secrets serial=4 path=/org/freedesktop/secrets/aliases/default; interface=org.freedesktop.Secret.Collection; member=SearchItems
array [
dict entry(
string "username"
string ""
)
dict entry(
string "service"
string "gh:github.com"
)
]
method return time=1681678963.133262 sender=:1.28 -> destination=:1.241 serial=1577 reply_serial=4
array [
object path "/org/freedesktop/secrets/collection/passwords/d72fb5ce71ea4196835b86ff53826c9b"
]
method call time=1681678963.133718 sender=:1.241 -> destination=org.freedesktop.secrets serial=5 path=/org/freedesktop/secrets; interface=org.freedesktop.Secret.Service; member=OpenSession
string "plain"
variant string ""
method return time=1681678963.134195 sender=:1.28 -> destination=:1.241 serial=1578 reply_serial=5
variant string ""
object path "/org/freedesktop/secrets/session/f4c480f8b9e64673ae53238eee74a4d6"
method call time=1681678963.134689 sender=:1.241 -> destination=org.freedesktop.secrets serial=6 path=/org/freedesktop/secrets/collection/passwords/d72fb5ce71ea4196835b86ff53826c9b; interface=org.freedesktop.Secret.Item; member=GetSecret
object path "/org/freedesktop/secrets/session/f4c480f8b9e64673ae53238eee74a4d6"
error time=1681678963.135092 sender=:1.28 -> destination=:1.241 error_name=org.freedesktop.Secret.Error.IsLocked reply_serial=6
method call time=1681678963.135611 sender=:1.241 -> destination=org.freedesktop.secrets serial=7 path=/org/freedesktop/secrets/session/f4c480f8b9e64673ae53238eee74a4d6; interface=org.freedesktop.Secret.Session; member=Close
method return time=1681678963.136162 sender=:1.28 -> destination=:1.241 serial=1580 reply_serial=7
method call time=1681678963.136718 sender=:1.241 -> destination=org.freedesktop.secrets serial=8 path=/org/freedesktop/secrets; interface=org.freedesktop.DBus.Properties; member=Get
string "org.freedesktop.Secret.Service"
string "Collections"
method return time=1681678963.137231 sender=:1.28 -> destination=:1.241 serial=1581 reply_serial=8
variant array [
object path "/org/freedesktop/secrets/collection/passwords"
]
method call time=1681678963.137773 sender=:1.241 -> destination=org.freedesktop.secrets serial=9 path=/org/freedesktop/secrets; interface=org.freedesktop.Secret.Service; member=Unlock
array [
object path "/org/freedesktop/secrets/aliases/default"
]
method return time=1681678963.138249 sender=:1.28 -> destination=:1.241 serial=1582 reply_serial=9
array [
object path "/org/freedesktop/secrets/collection/passwords"
]
object path "/"
method call time=1681678963.138731 sender=:1.241 -> destination=org.freedesktop.secrets serial=10 path=/org/freedesktop/secrets/aliases/default; interface=org.freedesktop.Secret.Collection; member=SearchItems
array [
dict entry(
string "service"
string "gh:github.com"
)
dict entry(
string "username"
string ""
)
]
method return time=1681678963.139639 sender=:1.28 -> destination=:1.241 serial=1583 reply_serial=10
array [
object path "/org/freedesktop/secrets/collection/passwords/d72fb5ce71ea4196835b86ff53826c9b"
]
method call time=1681678963.140248 sender=:1.241 -> destination=org.freedesktop.secrets serial=11 path=/org/freedesktop/secrets; interface=org.freedesktop.Secret.Service; member=OpenSession
string "plain"
variant string ""
method return time=1681678963.140984 sender=:1.28 -> destination=:1.241 serial=1584 reply_serial=11
variant string ""
object path "/org/freedesktop/secrets/session/3754810ae21f448091efecb69caf0fa5"
method call time=1681678963.141441 sender=:1.241 -> destination=org.freedesktop.secrets serial=12 path=/org/freedesktop/secrets/collection/passwords/d72fb5ce71ea4196835b86ff53826c9b; interface=org.freedesktop.Secret.Item; member=GetSecret
object path "/org/freedesktop/secrets/session/3754810ae21f448091efecb69caf0fa5"
error time=1681678963.142054 sender=:1.28 -> destination=:1.241 error_name=org.freedesktop.Secret.Error.IsLocked reply_serial=12
method call time=1681678963.142504 sender=:1.241 -> destination=org.freedesktop.secrets serial=13 path=/org/freedesktop/secrets/session/3754810ae21f448091efecb69caf0fa5; interface=org.freedesktop.Secret.Session; member=Close
method return time=1681678963.142976 sender=:1.28 -> destination=:1.241 serial=1586 reply_serial=13
Here's my test tool:
package main
import (
"fmt"
"os"
"github.com/zalando/go-keyring"
)
func main() {
var err error;
switch os.Args[1] {
case "set":
err = keyring.Set("gh:github.com", "foo", "aloha!")
case "get":
var secret string;
secret, err = keyring.Get("gh:github.com", "foo")
if err == nil {
fmt.Println(secret)
}
}
if err != nil {
fmt.Printf("err, %s\n", err)
}
}
These have a single password file opened and unlocked
Actual test cases have to go in comments because of github length restrictions ๐ผ
Go stuff:
> cat go.mod
module test
go 1.20
require (
github.com/alessio/shellescape v1.4.1 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/zalando/go-keyring v0.2.2 // indirect
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c // indirect
)
Keepassxc debug output:
KeePassXC - Version 2.7.4
Qt 5.15.8
Debugging mode is disabled.
Operating system: Arch Linux
CPU architecture: x86_64
Kernel: linux 6.2.8-arch1-1
Enabled extensions:
- Auto-Type
- Browser Integration
- SSH Agent
- KeeShare
- YubiKey
- Secret Service Integration
Cryptographic libraries:
- Botan 2.19.3
dbus-daemon is 1.14.6
, just in case.
Generally, I'm on arch linux with a lot of stuff, including keepassxc, gh and go, pulled in via nix and home-manager
I have another application that has to interact with keyring.
The application saves the keys in the format of account: "account_name" and service: "service_name" as can be seen on gui keyring manager.
But when i try to get the key with go-keyring it cannot find and neither can i set the account name with it. it only lets me set it in the service and username format.
anyways to enable this?
See this for inpiration: https://github.com/jaraco/keyring/blob/master/keyring/backends/Windows.py
In some environments without keyring or errors related to keyring setup, the user should be able to store their credentials into a encrypted file. This is one of the missing features from https://github.com/99designs/keyring which would complete this library
UPDATE: I managed to find a solution as I wrote this issue but thought it worth sharing in case someone else comes across it
This is similar to #8 that discusses headless operation, but this may be docker specific.
Error
When running example code from: https://github.com/zalando/go-keyring#linux it returns error:
failed to unlock correct collection '/org/freedesktop/secrets/aliases/default'
Steps to reproduce
docker run --privileged -it ubuntu:latest /bin/bash
apt update && apt install -y curl dbus-x11 gnome-keyring git vim golang
go get github.com/zalando/go-keyring
vi test.go
add code example from: https://github.com/zalando/go-keyring#linux
go run test.go
gnome keyring is running
root@fed2abd272d4:/# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 6320 1 0 15:09 ? 00:00:00 /usr/bin/dbus-daemon --syslog --fork --print-pid 4 --print-address 6 --session
root 6322 1 0 15:09 ? 00:00:00 /usr/bin/gnome-keyring-daemon --start --foreground --components=secrets
...
It seems it is possible to run headless using a python keyring library: https://keyring.readthedocs.io/en/latest/#using-keyring-on-headless-linux-systems
So, I followed the examples:
root@fed2abd272d4:/# dbus-run-session -- sh
# gnome-keyring-daemon --unlock
password
GNOME_KEYRING_CONTROL=/root/.cache/keyring-HWJLG0
SSH_AUTH_SOCK=/root/.cache/keyring-HWJLG0/ssh
# go run test.go
2020/03/01 15:12:43 secret
It is possible to set the username in wincred package Credential struct.
It does not add new features, only the Credential Manager will show the username properly.
Hey there! It appears to me that 99designs/keyring supports more backends compared to go-keyring. Other than that, are there any differences between both of them? Looks like 99designs/keyring also supports KWallet as well custom keyring names while go-keyring has open PRs for a while now.
At least in with git gnome keyring helper the github credentials are stored with user, server and protocol parameter so it's not possible to access password with hard coded username and service search parameters.
I was planning to overcome this issue by extending current interface with FreeGet
and FreeSet
functions which take instead of username and service the map of parameters.
What do you think about this approach?
It would be useful to allow injecting errors in the mock keyring provider to allow testing of error handling code.
#60 adds a test case for this. I'm unsure how this should be fixed correctly while maintaining the best possible backward compatibility. Options that came to mind:
I happily implement but would request some guidance on which direction it should be.
As of now, go-keyring only supports "plain" to transfer secrets.
It would be nice if go-keyring would support dh-ietf1024-sha256-aes128-cbc-pkcs7 as well as defined in here:
https://specifications.freedesktop.org/secret-service/latest/ch07s03.html
Further context: #66 (comment)
Kwallet recently added support for Secret Service API.
Looks like this does not work as "plain" Algorithm is not supported. Trying to use it, I get:
Algorithm plain is not supported. (only dh-ietf1024-sha256-aes128-cbc-pkcs7 is supported)
See: https://invent.kde.org/frameworks/kwallet/-/blob/master/src/runtime/kwalletd/kwalletfreedesktopservice.cpp#L265Probably either kwallet should add support for plain, or maybe
could be changed to use other algorithms as well.
I have a configuration file that holds many details that my app uses for various purposes such a SMTP password, db encryption token, etc. That configuration file is encrypted against a password using some golang code i wrote. Lets call it the "Config password"
Context and Intent:
This is a linux server in a data center with highly sensitive information stored on it.
The intent here is that if someone gets remote or physical access to the machine they cannot decrypt the configuration data stored in the config file.
Currently i store the password to decrypt the configuration file in Systemd file, and so anyone that gets physical access can find it.
How can i make it so that only that app can access the "Config password" that i stored into the TPM / KeyRing ?
The only way i can see this working is if the ServiceName and UserName is stored in the golang binary. It would then know the args to pass into go-keyring to retrieve the "Config password".
I feel like there is something i am missing as any attacker can decompile the golang binary and find the ServiceName and UserName.
fmt.Errorf is usually used with the first parameter being a format string.
In your case there is a plain string used, so errors.New(..) should suffice.
If you want to be more fancy, you may implement the error interface like this
type Error string
func (e Error) Error() string {
return string(e)
}
const (
ErrNotFound = Error("not found")
)
Or in other words: Everyone, that relies on keyring.ErrNotFound on Windows in a non-english setup, will fail, because "Element not found." does (surprisingly) not match e.g. "Element nicht gefunden."
/edit: sent mail according to the contribution guideline.
On Mac, the use of the security
utility to add a generic password to the Keychain insecurely passes the password on the command line, allowing any other process on the system to observe the password. To make it more secure, pass the password on STDIN to security
or use the Keychain API directly.
Hi,
I've found an issue while retrieving credentials set in the Windows Credential Manager by another application (not using go-keyring
).
Let's take this scenario, the main application sets a new credential to a specific service and username:
serviceName = service:name
username = $YOUR_USERNAME
When trying to call keyring.Get("service:name","$YOUR_USERNAME")
, the searched service name is created merging serviceName
and username
(searchedServiceName = service:name:$YOUR_USERNAME
), this behaviour is implemented in the function credName
.
This problem is the same also with keyring.Delete
and keyring.Set
.
Since changing this behavior would break every windows application using this library, I'd propose:
RawGet
, RawSet
and RawDelete
) that skips this behaviour on Windows (on other systems this would be a proxy to the original calls)NOOP
)I'm open to work on this as soon as a decision is taken.
Thanks ๐
Assumption taken in the following breaking change 41dbcc1 is problematic as it corrupts single line valid hex strings (that were not encoded) and returns them in a decoded form.
In this case it is not enough to check whenever dec, err := hex.DecodeString(trimStr)
does return an error to determine whenever value was stored in encoded form.
One of the propositions might be to perform decoding as an optional operation during obtaining stored value, or keep additional metadata after storing the value which will indicate whenever the value was encoded or not.
Test Case:
// TestGetSingleLineHexString tests getting a single line hex string password from the keyring.
func TestGetSingleLineHexString(t *testing.T) {
hexPassword := "abcdef123abcdef123"
err := Set(service, user, hexPassword)
if err != nil {
t.Errorf("Should not fail, got: %s", err)
}
pw, err := Get(service, user)
if err != nil {
t.Errorf("Should not fail, got: %s", err)
}
if hexPassword != pw {
t.Errorf("Expected password %s, got %s", hexPassword, pw)
}
}
Hello,
I have downloaded and tried this package under Centos7 (headless server) but the tests failed, any idea ?
[vagrant@localhost go-keyring]$ go test
--- FAIL: TestSet (0.04s)
keyring_test.go:15: Should not fail, got: The name org.freedesktop.secrets was not provided by any .service files
--- FAIL: TestGet (0.00s)
keyring_test.go:23: Should not fail, got: The name org.freedesktop.secrets was not provided by any .service files
keyring_test.go:28: Should not fail, got: The name org.freedesktop.secrets was not provided by any .service files
keyring_test.go:32: Expected password test-password, got
--- FAIL: TestGetNonExisting (0.00s)
keyring_test.go:40: Expected error ErrNotFound, got The name org.freedesktop.secrets was not provided by any .service files
--- FAIL: TestDelete (0.00s)
keyring_test.go:48: Should not fail, got: The name org.freedesktop.secrets was not provided by any .service files
FAIL
exit status 1
FAIL github.com/zalando/go-keyring 0.043s
Thanks :)
Github offers a lot of features via special .github
folder. Do you have any plan to adapt any of them? For example:
All of the above used by my repositories. You can check https://github.com/erdaltsksn/gh-label/tree/master/.github to see them.
NOTE: This app gh-label
uses go-keyring
so I link it.
If you are willing to adapt, I'm ready to work on it and create a pull request for it.
Really struggling using an app that uses this library on wslv2.
Installed keyring, but have no gnome, no gkeyring, no seahorse ... how can i create a keyring via the cli?
seahorse tells me:
(seahorse:23589): Gtk-WARNING **: 23:33:20.817: cannot open display:
gkeyring isn't maintained anymore
It would be nice if you could give me a hint, how i can initialize the default keyring via a terminal without x and gnome.
Update:
Found:
https://www.reddit.com/r/ProtonMail/comments/81zmto/linux_keyring_issues/
microsoft/WSL#4254
Ok, after installing some dbus, gnome keyring etc. i think this is working, even if it is no good solution:
dbus-run-session -- zsh (or your prefered sh)
gnome-keyring-daemon --unlock
<your password> return, ctrl+d
... use your program that relies on this library ...
if anyone knows how to "automate" this, please let me know
ok, best thin i found til now:
Add this to your bashrc, or zshrc or ...
if [ "$DBUS_SESSION_BUS_ADDRESS" = "" ]
then
exec dbus-run-session -- zsh
else
echo "$(/lib/cryptsetup/askpass 'Password: ')" | gnome-keyring-daemon --unlock
fi
!!! modify the exec statement to your usecase !!!
Cons:
This is the error I get when I try to run the binary with sudo
failed to unlock correct collection '/org/freedesktop/secrets/aliases/default'
Now, I don't know enough about keyring to know where the issue exactly is. Is it the issue with gnome-keyring or this lib?
Also, is there anything I can do to mitigate this problem?
The reason I'm running the binary with sudo is since it's being used to connect to vpn, and I can't to that without sudo.
I wanted to keep the username/password pair in keyring for future vpn connections. Is that even possible?
Ubuntu: 21.04
Gnome: 3.38.5
Hi, First of all, thanks to the maintainer team for the wonderful library.
Does go-keyring
provide a persistent mechanism? Is it cross-platform? e.g. I want to persist some secret which should remain across machine restarts for n number of days. I've observed that stored secrets were getting removed when restarting the machine. Is it possible to achieve this with this library?
Hiya! We have recently extracted a very similar library out of https://github.com/99designs/aws-vault into https://github.com/99designs/keyring.
We lack windows support, but we've got great macOS Keychain support. Any interest in perhaps collaborating and merging the two projects?
Hi zalando,
I'm using the Arch Linux distribution and don't use a desktop environment, just a window manager (AwesomeWM). I ran into an issue when using a tool that included go-keyring where it was returning "ERROR: failed to unlock correct collection". I didn't know what specific collection it was referring to, so I had to trace it back to here and saw that you included the helpful information in your readme. I believe it would be better to include the name of the collection you are trying to unlock so if anyone runs into the same problem, they don't have to track it down.
Thanks,
Joey
I am integrating this library in a project of mine and I am running into the issue that I want to bypass caching if it is not available locally.
A method added to the interface would be helpful. The method IsAvailable
would check if the service is available. I looked at the source and saw that it might have been already tried to add a function like IsAvailable
. I see a commented out function:
// func (*MacOSXKeychain) IsAvailable() bool {
// return exec.Command(execPathKeychain).Run() != exec.ErrNotFound
// }
Before I go into developing this more deeply, I was wondering if you tried to adding this method and have come into fundamental problems that prevent implementation.
We can also discuss if this is a good feature to be merged upstream into this library.
I noticed that in using the chanzuckerberg/aws-oidc project that there was a version upgrade to zalando/go-keyring v0.2.1.
It appears that in zalando/go-keyring v0.2.0, this change caused chanzuckerberg/aws-oidc to have this error.
Downgrading chanzuckerberg/aws-oidc back to zalando/go-keyring v0.1.1 fixed the problem but I don't think I understand context enough to why the accidentally breaking change was introduced in zalando/go-keyring v0.2.0
I would also like support for Pass with GPG instead of only supporting secretservice. Secretservice is a pain to work with on headless servers with no gui. Pass on the other hand is very simple to use via cli.
I can work on a PR to help speed things along.
First, thanks for this package!
There is a bug on Windows.
This line converts a byte slice to a Go string. This is fine if the byte slice is utf8, however Windows likes to store strings as utf16 it seems like the content of this blob is application defined. Nonetheless when the content is utf16, the result of the cast is an invalid Go string.
The fix would be to validate the byte slice and convert it to utf8 prior to casting it into a Go string.
Edit: I've looked into this somewhat and have realized there's no easy way to do this conversion in general, without knowing the encoding ahead of time.
Considering that FreeBSD has go 1.17.5, libsecret, gnome-keyring, and dbus-daemon, please consider adding support for FreeBSD
The version referenced in a go.mod is a pseudo-version based on the commit (v0.0.0-20200121091418-667557018717).
Pseudo-versions are a special type of pre-release version. Is it possible to release a stable API?
You can release v1.0.0 using git tags.
git tag v1.0.0
git push --tags
NOTE: In my projects, I prefer to use v0.1.0 as a initial version.
The purpose of .gitignore
file is to ensure that certain files not tracked by git remain untracked.
In 0.2.0, the file keyring_linux.go
was renamed to keyring_unix.go
to enable dbus on more platforms.
Unfortunately, the dbus implementation in godbus/dbus
requires cgo
for both dragonflybsd and freebsd. This means that if you are building with cgo disabled to build static binaries or for easier cross-compilation, upgrading to v0.2.0 results in build failures, with no way to disable this functionality.
One option would be to put a build tag constraint on cgo in keyring_unix.go
to not compile in the code if cgo is disabled. That would probably require a copy of the file to be available for non-dragonflybsd/freebsd platforms that still compiles if cgo is disabled.
Calling keyring.Set()
on Ubuntu 20.04 on WSL results in the following panic: panic: runtime error: slice bounds out of range
. On Ubuntu 18.04 on the same machine, keyring.Set()
fails more gracefully, returning the error exec: "dbus-launch": executable file not found in $PATH
. I've included a minimal PoC to reproduce.
OS: Microsoft Windows Server Version 1809
package main
import (
"fmt"
"github.com/zalando/go-keyring"
)
func main() {
if err := keyring.Set("my-service", "foo", "bar"); err != nil {
fmt.Printf("Error: %s", err)
} else {
fmt.Printf("Success!")
}
}
Ubuntu 20.04 on WSL:
thomas@windows-server-2019:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04 LTS
Release: 20.04
Codename: focal
thomas@windows-server-2019:~$ /mnt/c/Users/thomas/Desktop/keyring
panic: runtime error: slice bounds out of range [308:282]
goroutine 1 [running]:
github.com/godbus/dbus.getSessionBusPlatformAddress(0x59090f, 0x18, 0x0, 0x0)
/Users/thomas/Documents/projects/go/pkg/mod/github.com/godbus/[email protected]+incompatible/conn_other.go:30 +0x295
github.com/godbus/dbus.getSessionBusAddress(0x0, 0x0, 0x0, 0x0)
/Users/thomas/Documents/projects/go/pkg/mod/github.com/godbus/[email protected]+incompatible/conn.go:96 +0xf8
github.com/godbus/dbus.SessionBusPrivate(0x7f85a58ece20, 0xc00008cd18, 0x40d430)
/Users/thomas/Documents/projects/go/pkg/mod/github.com/godbus/[email protected]+incompatible/conn.go:101 +0x25
github.com/godbus/dbus.SessionBus(0x0, 0x0, 0x0)
/Users/thomas/Documents/projects/go/pkg/mod/github.com/godbus/[email protected]+incompatible/conn.go:73 +0xb5
github.com/zalando/go-keyring/secret_service.NewSecretService(0x40d430, 0xc00008cd80, 0x44bfa5)
/Users/thomas/Documents/projects/go/pkg/mod/github.com/zalando/[email protected]/secret_service/secret_service.go:50 +0x26
github.com/zalando/go-keyring.secretServiceProvider.Set(0x58d634, 0xa, 0x58c369, 0x3, 0x58c357, 0x3, 0x0, 0x0)
/Users/thomas/Documents/projects/go/pkg/mod/github.com/zalando/[email protected]/keyring_linux.go:14 +0x5d
github.com/zalando/go-keyring.Set(...)
/Users/thomas/Documents/projects/go/pkg/mod/github.com/zalando/[email protected]/keyring.go:27
main.main()
/Users/thomas/Documents/projects/go/src/github.com/DopplerHQ/cli/keyring-poc.go:10 +0x78
Ubuntu 18.04 on WSL:
thomas@windows-server-2019:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 18.04.2 LTS
Release: 18.04
Codename: bionic
thomas@windows-server-2019:~$ /mnt/c/Users/thomas/Desktop/keyring
Error: exec: "dbus-launch": executable file not found in $PATH
Is there a way to use a custom keyring (on linux) instead of 'login'?
Hello, I am from the GitHub CLI team and we use this library to store user tokens in secret storage. Historically we've had some issues around the CLI hanging when trying to interact with a secret service and recently we had a number of reports about the CLI failing to use the secret service on the first attempt after machine restart on linux.
After some investigation I believe the culprit is a race condition in how this library handles prompts. I'll explain the investigation in further detail below but the specific issue is in the following code, which runs when an attempt to Unlock
results in a prompt:
go-keyring/secret_service/secret_service.go
Lines 187 to 219 in b0e756d
This code makes a call with suffix .Prompt
, then registers a MatchSignal
for the successful completion of the prompt, at which point it blocks until it receives the signal with name suffix .Completed
. Once it receives the signal, unlocking can proceed and secrets can be interacted with.
I believe there is an issue here that the signal handling is registered after the .Prompt
call has been made. This leaves a short window for the .Completed
response to appear on the dbus before we have registered the handler. Typically, with the user filling in a password this would not be an issue, but if the user has set up automatic unlocking, it appears that the response can come fast enough to trigger a race.
Naively, I think this is as simple as delaying the .Prompt
call until right before we block awaiting the signal. I've opened a PR to demonstrate this: #105
I can't say for sure that it hasn't had some other unintended side effect but we've had three reports that it has resolved the original issue.
In this collapsed section you can find the dbus-monitor
log showing two attempts to use gh
in a way that accessed the secret service, right after machine restart. Below the details section I will detail the specific lines of interest.
signal time=1710441087.030272 sender=org.freedesktop.DBus -> destination=:1.76 serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired
string ":1.76"
signal time=1710441087.030324 sender=org.freedesktop.DBus -> destination=:1.76 serial=4 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameLost
string ":1.76"
method call time=1710441095.361593 sender=:1.77 -> destination=org.freedesktop.DBus serial=1 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=Hello
method return time=1710441095.361603 sender=org.freedesktop.DBus -> destination=:1.77 serial=1 reply_serial=1
string ":1.77"
signal time=1710441095.361606 sender=org.freedesktop.DBus -> destination=(null destination) serial=71 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameOwnerChanged
string ":1.77"
string ""
string ":1.77"
signal time=1710441095.361610 sender=org.freedesktop.DBus -> destination=:1.77 serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired
string ":1.77"
method call time=1710441095.361697 sender=:1.77 -> destination=org.freedesktop.secrets serial=2 path=/org/freedesktop/secrets; interface=org.freedesktop.DBus.Properties; member=Get
string "org.freedesktop.Secret.Service"
string "Collections"
method return time=1710441095.361955 sender=:1.18 -> destination=:1.77 serial=38 reply_serial=2
variant array [
object path "/org/freedesktop/secrets/collection/kdewallet"
]
method call time=1710441095.362390 sender=:1.77 -> destination=org.freedesktop.secrets serial=3 path=/org/freedesktop/secrets; interface=org.freedesktop.Secret.Service; member=Unlock
array [
object path "/org/freedesktop/secrets/aliases/default"
]
method return time=1710441095.362684 sender=:1.18 -> destination=:1.77 serial=39 reply_serial=3
array [
]
object path "/org/freedesktop/secrets/prompt/p0"
method call time=1710441095.362792 sender=:1.77 -> destination=org.freedesktop.secrets serial=4 path=/org/freedesktop/secrets/prompt/p0; interface=org.freedesktop.Secret.Prompt; member=Prompt
string ""
method return time=1710441095.362801 sender=:1.18 -> destination=:1.77 serial=40 reply_serial=4
signal time=1710441095.362957 sender=:1.18 -> destination=(null destination) serial=41 path=/modules/kwalletd5; interface=org.kde.KWallet; member=walletAsyncOpened
int32 1
int32 1966752915
signal time=1710441095.362963 sender=:1.18 -> destination=(null destination) serial=42 path=/modules/kwalletd6; interface=org.kde.KWallet; member=walletAsyncOpened
int32 1
int32 1966752915
signal time=1710441095.362965 sender=:1.18 -> destination=(null destination) serial=43 path=/org/freedesktop/secrets; interface=org.freedesktop.Secret.Service; member=CollectionChanged
object path "/org/freedesktop/secrets/collection/kdewallet"
signal time=1710441095.362967 sender=:1.18 -> destination=:1.77 serial=44 path=/org/freedesktop/secrets/prompt/p0; interface=org.freedesktop.Secret.Prompt; member=Completed
boolean false
variant array [
object path "/org/freedesktop/secrets/aliases/default"
]
method call time=1710441095.362970 sender=:1.77 -> destination=org.freedesktop.DBus serial=5 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=AddMatch
string "type='signal',path='/org/freedesktop/secrets/prompt/p0',interface='org.freedesktop.Secret.Prompt'"
method return time=1710441095.362972 sender=org.freedesktop.DBus -> destination=:1.77 serial=3 reply_serial=5
signal time=1710441098.632789 sender=org.freedesktop.DBus -> destination=:1.77 serial=4 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameLost
string ":1.77"
signal time=1710441098.632838 sender=org.freedesktop.DBus -> destination=(null destination) serial=72 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameOwnerChanged
string ":1.77"
string ":1.77"
string ""
method call time=1710441103.840776 sender=:1.78 -> destination=org.freedesktop.DBus serial=1 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=Hello
method return time=1710441103.840787 sender=org.freedesktop.DBus -> destination=:1.78 serial=1 reply_serial=1
string ":1.78"
signal time=1710441103.840803 sender=org.freedesktop.DBus -> destination=(null destination) serial=73 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameOwnerChanged
string ":1.78"
string ""
string ":1.78"
signal time=1710441103.840807 sender=org.freedesktop.DBus -> destination=:1.78 serial=2 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameAcquired
string ":1.78"
method call time=1710441103.840847 sender=:1.78 -> destination=org.freedesktop.secrets serial=2 path=/org/freedesktop/secrets; interface=org.freedesktop.DBus.Properties; member=Get
string "org.freedesktop.Secret.Service"
string "Collections"
method return time=1710441103.841219 sender=:1.18 -> destination=:1.78 serial=45 reply_serial=2
variant array [
object path "/org/freedesktop/secrets/collection/kdewallet"
]
method call time=1710441103.841232 sender=:1.78 -> destination=org.freedesktop.secrets serial=3 path=/org/freedesktop/secrets; interface=org.freedesktop.Secret.Service; member=Unlock
array [
object path "/org/freedesktop/secrets/aliases/default"
]
method return time=1710441103.841579 sender=:1.18 -> destination=:1.78 serial=46 reply_serial=3
array [
object path "/org/freedesktop/secrets/aliases/default"
]
object path "/"
method call time=1710441103.841591 sender=:1.78 -> destination=org.freedesktop.secrets serial=4 path=/org/freedesktop/secrets/aliases/default; interface=org.freedesktop.Secret.Collection; member=SearchItems
array [
dict entry(
string "username"
string ""
)
dict entry(
string "service"
string "gh:github.com"
)
]
method return time=1710441103.841886 sender=:1.18 -> destination=:1.78 serial=47 reply_serial=4
array [
object path "/org/freedesktop/secrets/collection/kdewallet/0"
]
method call time=1710441103.841893 sender=:1.78 -> destination=org.freedesktop.secrets serial=5 path=/org/freedesktop/secrets; interface=org.freedesktop.Secret.Service; member=OpenSession
string "plain"
variant string ""
method call time=1710441103.842137 sender=:1.18 -> destination=org.freedesktop.DBus serial=48 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=AddMatch
string "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0=':1.78'"
method return time=1710441103.842293 sender=:1.18 -> destination=:1.78 serial=49 reply_serial=5
variant array [
]
object path "/org/freedesktop/secrets/session/1"
method call time=1710441103.842300 sender=:1.78 -> destination=org.freedesktop.secrets serial=6 path=/org/freedesktop/secrets/collection/kdewallet/0; interface=org.freedesktop.Secret.Item; member=GetSecret
object path "/org/freedesktop/secrets/session/1"
method return time=1710441103.842411 sender=:1.18 -> destination=:1.78 serial=50 reply_serial=6
struct {
object path "/org/freedesktop/secrets/session/1"
array [
]
array of bytes "TOKEN-REDACTED"
string "text/plain; charset=utf8"
}
method call time=1710441103.842426 sender=:1.78 -> destination=org.freedesktop.secrets serial=7 path=/org/freedesktop/secrets/session/1; interface=org.freedesktop.Secret.Session; member=Close
method return time=1710441103.842569 sender=:1.18 -> destination=:1.78 serial=51 reply_serial=7
method call time=1710441103.842574 sender=:1.18 -> destination=org.freedesktop.DBus serial=52 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=RemoveMatch
string "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0=':1.78'"
method call time=1710441103.842576 sender=:1.18 -> destination=org.freedesktop.DBus serial=53 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=RemoveMatch
string "type='signal',sender='org.freedesktop.DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='org.freedesktop.DBus'"
error time=1710441103.842578 sender=org.freedesktop.DBus -> destination=:1.18 error_name=org.freedesktop.DBus.Error.MatchRuleNotFound reply_serial=53
string "The given match rule wasn't found and can't be removed"
signal time=1710441104.135900 sender=org.freedesktop.DBus -> destination=:1.78 serial=3 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameLost
string ":1.78"
signal time=1710441104.135944 sender=org.freedesktop.DBus -> destination=(null destination) serial=74 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameOwnerChanged
string ":1.78"
string ":1.78"
string ""
In the first usage of gh
we see the call to .Unlock
returning with a need to prompt:
method call time=1710441095.362390 sender=:1.77 -> destination=org.freedesktop.secrets serial=3 path=/org/freedesktop/secrets; interface=org.freedesktop.Secret.Service; member=Unlock
array [
object path "/org/freedesktop/secrets/aliases/default"
]
method return time=1710441095.362684 sender=:1.18 -> destination=:1.77 serial=39 reply_serial=3
array [
]
object path "/org/freedesktop/secrets/prompt/p0"
Immediately after this we see the call to .Prompt
:
method call time=1710441095.362792 sender=:1.77 -> destination=org.freedesktop.secrets serial=4 path=/org/freedesktop/secrets/prompt/p0; interface=org.freedesktop.Secret.Prompt; member=Prompt
string ""
method return time=1710441095.362801 sender=:1.18 -> destination=:1.77 serial=40 reply_serial=4
Then shortly after we see the .Completed
response:
signal time=1710441095.362967 sender=:1.18 -> destination=:1.77 serial=44 path=/org/freedesktop/secrets/prompt/p0; interface=org.freedesktop.Secret.Prompt; member=Completed
boolean false
variant array [
object path "/org/freedesktop/secrets/aliases/default"
]
And following that we see the attempt to AddMatch
on this interface:
method call time=1710441095.362970 sender=:1.77 -> destination=org.freedesktop.DBus serial=5 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=AddMatch
string "type='signal',path='/org/freedesktop/secrets/prompt/p0',interface='org.freedesktop.Secret.Prompt'"
3 seconds later (which is the length of time the CLI waits before reporting a timeout), we see the client close:
signal time=1710441098.632789 sender=org.freedesktop.DBus -> destination=:1.77 serial=4 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=NameLost
string ":1.77"
In the second attempt to use gh
, there is no prompt required and everything completes as expected:
method call time=1710441103.842300 sender=:1.78 -> destination=org.freedesktop.secrets serial=6 path=/org/freedesktop/secrets/collection/kdewallet/0; interface=org.freedesktop.Secret.Item; member=GetSecret
object path "/org/freedesktop/secrets/session/1"
method return time=1710441103.842411 sender=:1.18 -> destination=:1.78 serial=50 reply_serial=6
struct {
object path "/org/freedesktop/secrets/session/1"
array [
]
array of bytes "TOKEN-REDACTED"
string "text/plain; charset=utf8"
}
ref kopia/kopia#732
I got a panic in Kopia, which uses go-keyring:
> "kopia.exe" snapshot create --description "abcdefg" "C:\some\path"
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x38 pc=0x136cc7a]
goroutine 1 [running]:
github.com/zalando/go-keyring.windowsKeychain.Get(0xc000167860, 0x22, 0xc00042f2ee, 0x8, 0x1a4eba0, 0xc00042f2e0, 0x1a3b740, 0xc00050f880)
/home/travis/gopath/pkg/mod/github.com/zalando/[email protected]/keyring_windows.go:20 +0x11a
github.com/zalando/go-keyring.Get(...)
/home/travis/gopath/pkg/mod/github.com/zalando/[email protected]/keyring.go:32
github.com/kopia/kopia/repo.GetPersistedPassword(0x1d8fbc0, 0xc000534120, 0xc0004be200, 0x39, 0xc00050f7e0, 0x0, 0xc000534150)
/home/travis/gopath/src/github.com/kopia/kopia/repo/password.go:26 +0x375
github.com/kopia/kopia/cli.getPasswordFromFlags(0x1d8fbc0, 0xc000534120, 0x1d70100, 0xc0000720d0, 0x0, 0x0, 0x1a4cd60)
/home/travis/gopath/src/github.com/kopia/kopia/cli/password.go:56 +0x145
github.com/kopia/kopia/cli.openRepository(0x1d8fbc0, 0xc000534120, 0x0, 0x1, 0x0, 0x0, 0x0, 0x0)
/home/travis/gopath/src/github.com/kopia/kopia/cli/config.go:60 +0x165
github.com/kopia/kopia/cli.maybeRepositoryAction.func1.1(0x0, 0x0)
/home/travis/gopath/src/github.com/kopia/kopia/cli/app.go:141 +0xf0
github.com/kopia/kopia/cli.withProfiling(...)
/home/travis/gopath/src/github.com/kopia/kopia/cli/profile_disabled.go:7
github.com/kopia/kopia/cli.maybeRepositoryAction.func1(0xc0001a2d80, 0xe8bcaa, 0x1ac45e0)
/home/travis/gopath/src/github.com/kopia/kopia/cli/app.go:125 +0x64
gopkg.in/alecthomas/kingpin%2ev2.(*actionMixin).applyActions(0xc0004f9518, 0xc0001a2d80, 0x0, 0x0)
/home/travis/gopath/pkg/mod/gopkg.in/alecthomas/[email protected]/actions.go:28 +0x74
gopkg.in/alecthomas/kingpin%2ev2.(*Application).applyActions(0xc000152870, 0xc0001a2d80, 0x0, 0x0)
/home/travis/gopath/pkg/mod/gopkg.in/alecthomas/[email protected]/app.go:557 +0xe3
gopkg.in/alecthomas/kingpin%2ev2.(*Application).execute(0xc000152870, 0xc0001a2d80, 0xc00050f620, 0x2, 0x2, 0x0, 0x0, 0x0, 0xc000007dd8)
/home/travis/gopath/pkg/mod/gopkg.in/alecthomas/[email protected]/app.go:390 +0xa5
gopkg.in/alecthomas/kingpin%2ev2.(*Application).Parse(0xc000152870, 0xc000130010, 0x5, 0x7, 0x1, 0xc000007de0, 0x0, 0x1)
/home/travis/gopath/pkg/mod/gopkg.in/alecthomas/[email protected]/app.go:222 +0x213
main.main()
/home/travis/gopath/src/github.com/kopia/kopia/main.go:77 +0x188
It crashes here in line 20:
Lines 11 to 20 in 6905df4
Now, I don't know much about golang, but one thing that might be related is that go-keyring checks whether err != nil
, whereas wincred checks if cred != nil
.
In case err
was nil
and ret
was 0
here https://github.com/danieljoos/wincred/blob/78f93c1f8b99b0c2f6e7f3d2bdc4993cf87bddff/sys.go#L70, I believe that it would cause nil, nil
to be returned.
Another way nil, nil
might be returned was if cred
is null:
https://github.com/danieljoos/wincred/blob/78f93c1f8b99b0c2f6e7f3d2bdc4993cf87bddff/conversion.go#L91-L94
See danieljoos/wincred#5 (comment).
So I'd change the check from if err != nil
to if cred == nil
.
That wouldn't solve the underlying issue, but might convert the panic into a normal, handleable error.
Update:
I took a look at the logs, and, running on two different computers, I had two errors out of 290 runs (0.69%).
Each time it was executing a bunch of times successfully in sequence and then failed.
Ref danieljoos/wincred#5 (comment), the CreadRead API seems to be a bit flakey, do you think it might make sense to check if cred, err
are nil, nil
, and then try a second time, or would that be better handled at the calling application (Kopia in this case)?
Update 2:
4 failures in 439 runs (0.91%)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.