Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prompt to unlock individual item when getting secret from keyring in unix system #108

Merged
merged 2 commits into from
Jun 6, 2024

Conversation

AlanD20
Copy link
Contributor

@AlanD20 AlanD20 commented May 30, 2024

Hi, this PR unlocks individual items before getting secret from the keyring for unix systems.

Why?

I'm using KeepassXC on Arch as a secret service. And I was trying to setup gh CLI where it requires git credentials, and I wanted to use libsecret to store the credentials, so I updated my git config:

[credential]
  helper = !/usr/lib/git-core/git-credential-libsecret

In my KeepassXC settings, I have a checkbox checked where, by default it locks all collections and items. A client has to ask for unlock (a prompt) so that I will allow the client to retrieve the secret from the item or the collection.

KeepassXC => Tools > Settings > Secret Service Integration > Confirm when passwords are retrieved by clients
image

gh CLI is able to use the libsecret with my KeepassXC as a secret service but after debugging and using dbus-monitor and busctl monitor --user to see what the requests are.

I found this error from `dbus-monitor` (Exapndable)
method call time=1717061633.905718 sender=:1.554 -> 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 call time=1717061633.905779 sender=:1.463 -> destination=org.freedesktop.DBus serial=3485 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=GetConnectionUnixProcessID
   string ":1.554"
method return time=1717061633.905787 sender=org.freedesktop.DBus -> destination=:1.463 serial=103 reply_serial=3485
   uint32 863002
method call time=1717061633.906061 sender=:1.463 -> destination=org.freedesktop.DBus serial=3486 path=/org/freedesktop/DBus; interface=org.freedesktop.DBus; member=GetConnectionUnixProcessID
   string ":1.554"
method return time=1717061633.906069 sender=org.freedesktop.DBus -> destination=:1.463 serial=104 reply_serial=3486
   uint32 863002
method call time=1717061633.906348 sender=:1.463 -> destination=org.freedesktop.DBus serial=3487 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.554',arg2=''"
method return time=1717061633.906499 sender=:1.463 -> destination=:1.554 serial=3488 reply_serial=2
   variant       array [
         object path "/org/freedesktop/secrets/collection/db_2Dsecrets"
      ]
method call time=1717061633.906623 sender=:1.554 -> destination=org.freedesktop.secrets serial=3 path=/org/freedesktop/secrets; interface=org.freedesktop.Secret.Service; member=Unlock  <<< COLLECTION IS UNLOCKED>>>
   array [
      object path "/org/freedesktop/secrets/aliases/default"
   ]
method return time=1717061633.906641 sender=:1.463 -> destination=:1.554 serial=3489 reply_serial=3
   array [
      object path "/org/freedesktop/secrets/collection/db_2Dsecrets"
   ]
   object path "/"
method call time=1717061633.906708 sender=:1.554 -> 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=1717061633.906890 sender=:1.463 -> destination=:1.554 serial=3490 reply_serial=4
   array [
      object path "/org/freedesktop/secrets/collection/db_2Dsecrets/5e85bdec38db4f9fa00a4a00004e0977"
   ]
method call time=1717061633.907022 sender=:1.554 -> destination=org.freedesktop.secrets serial=5 path=/org/freedesktop/secrets; interface=org.freedesktop.Secret.Service; member=OpenSession
   string "plain"
   variant       string ""
method return time=1717061633.907143 sender=:1.463 -> destination=:1.554 serial=3491 reply_serial=5
   variant       string ""
   object path "/org/freedesktop/secrets/session/42b891c426fa4ddab4ca9d5b98096369"
method call time=1717061633.907231 sender=:1.554 -> destination=org.freedesktop.secrets serial=6 path=/org/freedesktop/secrets/collection/db_2Dsecrets/5e85bdec38db4f9fa00a4a00004e0977; interface=org.freedesktop.Secret.Item; member=GetSecret <<<ATTEMPTING TO GET SECRET>>>
   object path "/org/freedesktop/secrets/session/42b891c426fa4ddab4ca9d5b98096369"
error time=1717061633.907283 sender=:1.463 -> destination=:1.554 error_name=org.freedesktop.Secret.Error.IsLocked reply_serial=6 <<<REJECTS DUE TO ITEM IS LOCKED>>>
method call time=1717061633.907338 sender=:1.554 -> destination=org.freedesktop.secrets serial=7 path=/org/freedesktop/secrets/session/42b891c426fa4ddab4ca9d5b98096369; interface=org.freedesktop.Secret.Session; member=Close
method return time=1717061633.907424 sender=:1.463 -> destination=:1.554 serial=3493 reply_serial=7

Looking closely at the requests here, it appears that go-keyring is able to unlock the collection which is done at

func (s secretServiceProvider) findItem(svc *ss.SecretService, service, user string) (dbus.ObjectPath, error) {
collection := svc.GetLoginCollection()
search := map[string]string{
"username": user,
"service": service,
}
err := svc.Unlock(collection.Path())
if err != nil {
return "", err
}
but when it tries to get the secret, since I have the KeepassXC option enabled where the items are also locked, go-keyring does not attempt to unlock the item, and tries to get the secret. specifically at these log output in the dbus-monitor:

path=/org/freedesktop/secrets; interface=org.freedesktop.Secret.Service; member=OpenSession
   string "plain"
   variant       string ""
method return time=1717061633.907143 sender=:1.463 -> destination=:1.554 serial=3491 reply_serial=5
   variant       string ""
   object path "/org/freedesktop/secrets/session/42b891c426fa4ddab4ca9d5b98096369"
method call time=1717061633.907231 sender=:1.554 -> destination=org.freedesktop.secrets serial=6 path=/org/freedesktop/secrets/collection/db_2Dsecrets/5e85bdec38db4f9fa00a4a00004e0977; interface=org.freedesktop.Secret.Item; member=GetSecret <<<ATTEMPTING TO GET SECRET>>>
   object path "/org/freedesktop/secrets/session/42b891c426fa4ddab4ca9d5b98096369"
error time=1717061633.907283 sender=:1.463 -> destination=:1.554 error_name=org.freedesktop.Secret.Error.IsLocked reply_serial=6 <<<REJECTS DUE TO ITEM IS LOCKED>>>
method call time=1717061633.907338 sender=:1.554 -> destination=org.freedesktop.secrets serial=7 path=/org/freedesktop/secrets/session/42b891c426fa4ddab4ca9d5b98096369; interface=org.freedesktop.Secret.Session; member=Close

PR Changes

My changes attempt to unlock the item before getting the secret, which I have tried locally, and it works with gh cli as well. (see below to reproduce my local environment and my test steps.)

Looking at the secret-service documentation and specifically the first line says:

Some items and/or collections may be marked as locked by the service. The secrets of locked items cannot be accessed. Additionally, locked items or collections cannot be modified by the client application.

and the fifth line:

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.

How is gh cli related to this?

A cli/cli#8691 (comment) by GitHub CLI team confirms that they are not doing anything and simply using go-keyring library to set and get these secrets from keyrings. I have also confirmed locally that they use go-keyring wrapper here: https://github.com/cli/cli/blob/trunk/internal/keyring/keyring.go

What issue does it resolve?

probably other packages that use go-keyring as a dependency. I'm only using gh which uses go-keyring as a primary dependency for getting secrets from keyring.

Test Steps to Reproduce

  1. Setup KeepassXC as a secret service.
  2. Enable Confirm when passwords are retrieved by clients at KeepassXC => Tools > Settings > Secret Service Integration > Confirm when passwords are retrieved by clients
  3. Install gh cli and authenticate using https protocol.
  4. Configure git to use libsecret, for arch users, it already has git-credentials-libsecret at /usr/lib/git-core/git-credential-libsecret.
    git config --global credential.helper libsecret
    # or absolute path
    git config --global credential.helper /usr/lib/git-core/git-credential-libsecret
  5. Run dbus-monitor to see secret service requests.
  6. check gh authentication status: gh auth status. (This will fail and says not authenticated)

Test PR Changes locally

  • working directory is ~/builds and the following steps will clone 2 projects in the ~/builds directory, and also builds gh cli locally at ~/builds as well.
  1. First try to reproduce the error to create the environment. and make sure KeepassXC, git and gh cli are properly setup. Because this will confirm the PR changes resolve the issue.
  2. Clone PR changes locally.
  3. Clone gh cli repository at https://github.com/cli/cli then cd into your local gh cli repository
  4. Modify the go.mod file to point the go-keyring package to the PR changes. My changes are at ../go-keyring relative to the cloned gh cli repository locally.
    module github.com/cli/cli/v2
    
    go 1.22
    
    # add this line to point to the local go-keyring changes
    replace github.com/zalando/go-keyring => ../go-keyring
  5. Run go mod tidy just in case.
  6. Build gh cli locally, Here is the doc, but it should be fairly simple by running make install prefix=../gh
  7. Run the new gh cli binary with go-keyring changes: cd ~builds/gh/bin && ./gh auth status
  8. A KeepassXC prompt appears that asks for confirmation by user, and after confirming, gh authentication succeeds.

Notes

  • I'm not very familiar with all of these, let me know if I have missed anything.
  • Feel free to review the test cases that I have modified for the mock provider.

Thanks!

@AlanD20
Copy link
Contributor Author

AlanD20 commented May 31, 2024

After looking into it more, looks like the mock provider changes will be a breaking change. Not sure what to do here. Probably I should remove the mock changes, or should it unlock whenever it gets a secret without the user explicitly unlocking the item?

@mikkeloscar
Copy link
Member

@AlanD20 Thanks for the PR and the detailed description. Please allow a few days for us to review it.

@mikkeloscar mikkeloscar self-assigned this May 31, 2024
@mikkeloscar mikkeloscar self-requested a review May 31, 2024 09:06
@mikkeloscar mikkeloscar removed their assignment May 31, 2024
keyring_mock.go Outdated Show resolved Hide resolved
err = svc.Unlock(item)
if err != nil {
return "", err
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be awesome if it's possible to create a test case showing this working as expected. You can see how we do the tests currently: https://github.com/zalando/go-keyring/blob/master/.github/workflows/go.yml

Copy link
Contributor Author

@AlanD20 AlanD20 May 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I figured out how to reproduce using the test cases but I dont know how to write the test cases for it because it's a bit tricky.

basically, a test case has to get an existing secret from the keyring. The issue with the existing TestGet case is that it initially sets the secret which it already has an open/unlocked session, and then it gets the secret, which is totally fine.

But in the case of KeepassXC, where it has the option to lock items, when the session gets closed and another session opens to only retrieve the secret, it requires a prompt from the user to confirm.

I enabled gnome-keyring-daemon locally and ran the test case but it seems to work because it looks like the gnome-keyring-daemon does not have an option to prompt when a secret is retrieved.

If you have KeepassXC enabled as a secret service. Make sure to enable the prompt option, then try this test case:

func TestGetLockedItem(t *testing.T) {
   pw, err := Get(service, "locked-user")
   if err != nil {
      if err == ErrNotFound {
         err := Set(service, "locked-user", password)
         if err != nil {
            t.Errorf("Should not fail, got: %s", err)
         }
         t.Skip("Skipping: Secret stored in keyring, rerun tests to confirm test case.")
      }

      t.Errorf("Should not fail, got: %s", err)
   }

   if password != pw {
      t.Errorf("Expected password %s, got %s", password, pw)
   }
}

This test case has to be run twice. First to set the secret if it's not already set, and then if it's already set, get the secret. I dont like this since it requires to run go test twice.

Any idea how to test this? 😅

here is also a video

go-keyring-test-case.mp4

@AlanD20 AlanD20 requested a review from mikkeloscar June 4, 2024 18:03
@mikkeloscar
Copy link
Member

👍

@mikkeloscar mikkeloscar requested a review from szuecs June 5, 2024 14:19
@szuecs
Copy link
Member

szuecs commented Jun 6, 2024

👍

@szuecs szuecs merged commit de351c5 into zalando:master Jun 6, 2024
7 checks passed
@AlanD20
Copy link
Contributor Author

AlanD20 commented Jun 6, 2024

Thanks for the help and review!

Will there be a new release for this? I'm planning to create a new PR in gh cli repository to use this change so that it will get fixed.

@mikkeloscar
Copy link
Member

@AlanD20 New release here: https://github.com/zalando/go-keyring/releases/tag/v0.2.5

@AlanD20
Copy link
Contributor Author

AlanD20 commented Jun 7, 2024

@mikkeloscar Awesome! Thanks again :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants