1
0
mirror of https://github.com/docker/docker-credential-helpers.git synced 2026-06-28 15:21:29 +05:30

Compare commits

...

139 Commits

Author SHA1 Message Date
David Scott fc9290adbc Merge pull request #203 from ebriney/bump_to_v0.6.4
Bump to 0.6.4
2021-06-02 11:34:05 +01:00
Emmanuel Briney 8369960895 Merge pull request #205 from thaJeztah/fix_ci2
travis: temporarily disabling ppc64le, and fix some linting issues
2021-06-01 12:36:26 +02:00
Sebastiaan van Stijn e4a625d24b travis: temporarily disable ppc
The ppc machines use systemd, so starting xvfb  doesnt work

    +sh -e /etc/init.d/xvfb start
    sh: 0: Can't open /etc/init.d/xvfb

Temporarily disabling it for now

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-06-01 10:36:09 +02:00
Sebastiaan van Stijn f552261f32 travis: fix selection of ppc64le
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-06-01 10:22:13 +02:00
Sebastiaan van Stijn 7a2694fc98 fix some linting issues
/go/src/github.com/docker/docker-credential-helper/pass/pass.go:1:1: package comment should be of the form "Package pass ..."
    /go/src/github.com/docker/docker-credential-helper/pass/pass.go:22:7: don't use ALL_CAPS in Go names; use CamelCase
    /go/src/github.com/docker/docker-credential-helper/pass/pass.go:22:7: exported const PASS_FOLDER should have comment or be unexported
    /go/src/github.com/docker/docker-credential-helper/pass/pass.go:82:1: receiver name h should be consistent with previous receiver name p for Pass
    /go/src/github.com/docker/docker-credential-helper/pass/pass.go:94:1: receiver name h should be consistent with previous receiver name p for Pass
    /go/src/github.com/docker/docker-credential-helper/pass/pass.go:131:1: receiver name h should be consistent with previous receiver name p for Pass
    /go/src/github.com/docker/docker-credential-helper/pass/pass.go:161:1: receiver name h should be consistent with previous receiver name p for Pass
    /go/src/github.com/docker/docker-credential-helper/registryurl/url_go18.go:9:1: exported function GetHostname should have comment or be unexported
    /go/src/github.com/docker/docker-credential-helper/registryurl/url_go18.go:13:1: exported function GetPort should have comment or be unexported
    /go/src/github.com/docker/docker-credential-helper/secretservice/secretservice_linux_test.go:22:2: don't use underscores in Go names; var old_auths should be oldAuths
    /go/src/github.com/docker/docker-credential-helper/secretservice/secretservice_linux_test.go:64:2: don't use underscores in Go names; var new_auths should be newAuths

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-06-01 10:22:11 +02:00
Emmanuel Briney 4b9fe97b79 Merge pull request #187 from espindola/openbsd
Enable pass on non-linux systems
2021-05-31 14:23:42 +02:00
ebriney 0232333efa Bump to 0.6.4
Signed-off-by: ebriney <emmanuel.briney@docker.com>
2021-05-27 17:11:30 +02:00
Tibor Vass 38bea2ce27 Merge pull request #195 from tiborvass/execabs
Use golang.org/x/sys/execabs
2021-01-25 09:24:08 -08:00
Tibor Vass c9a35c136e Merge pull request #168 from thaJeztah/go_mod
Add go.mod, and vendor wincred v1.1.0
2021-01-25 09:18:51 -08:00
Tibor Vass 431b64c703 Use golang.org/x/sys/execabs
Signed-off-by: Tibor Vass <tibor@docker.com>
2021-01-21 20:14:54 +00:00
Tibor Vass 951b97d7f5 vendor: update wincred to v1.1.0
Signed-off-by: Tibor Vass <tibor@docker.com>
2021-01-21 20:10:42 +00:00
Sebastiaan van Stijn f7d32862eb Add go.mod, and vendor wincred v1.0.0
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2021-01-21 20:08:59 +00:00
Sebastiaan van Stijn 699f1d6790 Merge pull request #193 from tonistiigi/osx-message
osxkeychain: improve error message for non-interactive sessions
2021-01-20 10:56:22 +01:00
Tonis Tiigi f4b8a8531e osxkeychain: improve error message for non-interactive sessions
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2021-01-07 11:38:06 -08:00
Rafael Ávila de Espíndola f76f0b3f33 Enable pass on non-linux systems
There is nothing linux specific about pass. In particular, I use it in
openbsd.

Signed-off-by: Rafael Ávila de Espíndola <rafael@espindo.la>
2020-09-14 20:54:21 -07:00
Guillaume Rose f78081d1f7 Merge pull request #161 from StefanScherer/update-windows-build-2019-pfx
[DESKTOP-1469] Update Windows Authenticode certificate
2019-07-20 08:39:34 +02:00
Stefan Scherer 4e7d4a98a4 Update Windows Authenticode pfx
Signed-off-by: Stefan Scherer <stefan.scherer@docker.com>
2019-07-18 08:37:50 +02:00
Guillaume Rose 43d1f2919f Merge pull request #159 from guillaumerose/release
Cleanup release process. Create tarballs in Jenkins.
2019-07-17 16:11:26 +02:00
Guillaume Rose 7b072d4d2e Merge pull request #160 from ghatwala/master
Attempt to build this package on power
2019-07-17 16:11:16 +02:00
ghatwala c0c41f47b2 Attempt to build this package on power
Adding power support in travis.yml
- Adding ppc64le build and running "before_script_linux.sh" on intel only via travis.yml

Signed-off-by: ghatwala <ghatwala@us.ibm.com>
2019-07-17 14:58:17 +05:30
Guillaume Rose 69fb197018 Don’t use binaries built by Travis. Use Jenkins binaries.
Signed-off-by: Guillaume Rose <guillaume.rose@docker.com>
2019-07-17 10:35:02 +02:00
Guillaume Rose 86b653a9f1 Create tarballs in Jenkins.
Signed-off-by: Guillaume Rose <guillaume.rose@docker.com>
2019-07-17 10:34:50 +02:00
Guillaume Rose 88f932172b Remove old appveyor configuration
Signed-off-by: Guillaume Rose <guillaume.rose@docker.com>
2019-07-17 10:12:27 +02:00
Guillaume Rose 54f0238b6b Merge pull request #157 from guillaumerose/bump
Bump to 0.6.3
2019-07-16 13:54:57 +02:00
Guillaume Rose 2bf42cfd47 Bump to 0.6.3
Signed-off-by: Guillaume Rose <guillaume.rose@docker.com>
2019-07-16 10:46:07 +02:00
Guillaume Rose 1c9f7ede70 Merge pull request from GHSA-g9w7-h2f5-5vp2
Fix a double free in the List functions
2019-07-16 09:57:38 +02:00
Justin Cormack 87c80bfba5 Fix a double free in the List functions
The code was set up so that it would free the individual items and the data
in `freeListData`, but there was already a Go `defer` to free the data item,
resulting in a double free.

Remove the `free` in `freeListData` and leave the original one.

In addition, move the `defer` for freeing the list data before the error
check, so that the data is also free in the error case. This just removes
a minor leak.

This vulnerability was discovered by:
Jasiel Spelman of Trend Micro Zero Day Initiative and Trend Micro Team Nebula

Signed-off-by: Justin Cormack <justin.cormack@docker.com>
2019-07-01 14:41:30 +01:00
Silvin Lubecki 680ca48e6d Merge pull request #153 from thaJeztah/merge_0.6.2
Merge 'v0.6.2' to master
2019-06-20 14:53:21 +02:00
Sebastiaan van Stijn f6d4261609 Merge 'v0.6.2' to master
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2019-06-20 11:51:07 +02:00
Guillaume Rose f755519945 Merge pull request #148 from guillaumerose/jenkinsfile
Add Jenkinsfile
2019-05-07 08:12:18 -07:00
Guillaume Rose 11d9f9dba7 Add Jenkinsfile
Signed-off-by: Guillaume Rose <guillaume.rose@docker.com>
2019-05-07 16:04:36 +02:00
Ulrich VACHON beda055c57 Merge pull request #145 from ulrich/bump-to-v0.6.2
Bump version to 0.6.2
2019-05-02 12:50:58 +02:00
Ulrich VACHON 22b8706efa Bump version to 0.6.2
Signed-off-by: Ulrich VACHON <ulrich.vachon@docker.com>
2019-05-02 12:41:03 +02:00
Ulrich VACHON 8a9f93a99f Bump version to 0.6.2
Signed-off-by: Ulrich VACHON <ulrich.vachon@docker.com>
2019-05-02 12:10:16 +02:00
Mathieu Champlon 063cca0a6d Merge pull request #97 from s864372002/hotfix/typo
Typo fixed.
2019-04-30 00:43:22 -07:00
Vincent Demeester 152d64310b Merge pull request #139 from ekcasey/server-alias-wincred
make docker-credential-wincred work like docker-credential-osxkeychain
2019-04-30 09:10:39 +02:00
Emily Casey 77e30bd9dd Style fixes - incorporates PR feedback
Signed-off-by: Emily Casey <ecasey@pivotal.io>
Signed-off-by: Danny Joyce <djoyce@pivotal.io>
2019-04-29 10:51:04 -04:00
Guillaume Tardif 74636a1592 Merge pull request #143 from pgayvallet/osx-list-no-error-on-missing-key
Fix docker-credential-osxkeychain list behaviour in case of missing entry in keychain
2019-04-29 16:27:28 +02:00
Emily Casey a3c1b5b757 Fix imports
Signed-off-by: Emily Casey <ecasey@pivotal.io>
2019-04-29 10:04:46 -04:00
Emily Casey 6f4b0a7c06 make docker-credential-wincred work like docker-credential-osxkeychain
* fetch credentials for server with matching hostname if scheme, path, or port are not provided
* if the credential request includes specific scheme, path, or port that does not match entry, don't return
* extract url helpers into a package

Signed-off-by: Emily Casey <ecasey@pivotal.io>
Signed-off-by: Danny Joyce <djoyce@pivotal.io>
2019-04-29 09:26:13 -04:00
pgayvallet 1546024a83 returns empty map instead of error if credentials not found in keychain
Signed-off-by: pgayvallet <pierre.gayvallet@gmail.com>
2019-04-26 18:34:58 +02:00
Sebastiaan van Stijn ecb01138bd Merge pull request #142 from ulrich/fix_travis_ci
Fix Travis configuration, update XCode version, Golang version and set to minimum macOS version to 10.11.
2019-04-26 09:27:06 -07:00
Ulrich VACHON df92c83808 Update Go version to 1.12.x, update XCode version to 10.1, update MacOS minimum supported version to 10.11.
Signed-off-by: Ulrich VACHON <ulrich.vachon@docker.com>
2019-04-26 12:00:35 +02:00
Vincent Demeester 123ba1b7cd Merge pull request #124 from eyJhb/master
pass: changed the way for checking if password-store is initalized
2018-09-25 10:51:22 +02:00
eyjhbb@gmail.com 9c18f033f7 pass: changed error message returned, if password-store is not initialized
Signed-off-by: eyjhbb@gmail.com <eyjhbb@gmail.com>
2018-09-24 15:15:24 +02:00
eyjhbb@gmail.com d6c1f136e4 pass: changed the way for checking if password-store is initalized
Signed-off-by: eyjhbb@gmail.com <eyjhbb@gmail.com>
2018-09-22 12:53:56 +02:00
Vincent Demeester 73e5f5dbfe Merge pull request #29 from dekkagaijin/freefix
C.free(unsafe.Pointer(err)) -> C.g_error_free(err)
2018-07-19 09:47:51 +02:00
Nassim Eddequiouaq 5241b46610 Merge pull request #110 from euank/lazy-init
pass: only init on run, and do so lazily
2018-06-27 14:33:29 +02:00
Vincent Demeester 3cba3913ea pass: add IsInitialized helper
This will be useful for the cli where they check initialization:
https://github.com/docker/cli/blob/be8dab26a3ab589b96788fdb95f3d07378e57b9b/cli/config/credentials/default_store_linux.go#L8-L10

Signed-off-by: Euan Kemp <euank@euank.com>
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-06-27 14:28:47 +02:00
Vincent Demeester 8502b53592 Merge pull request #108 from euank/pass-trim‮‮‮‮‮‮‮‮trim-pass
pass: trim pass show output
2018-06-27 14:24:08 +02:00
Euan Kemp 5da09fd251 pass: only init on run, and do so lazily
This also fixes the following issues:

1. Safe for concurrent initialization still (it was before in 'init',
   but the alternative to this PR is not)
2. Uses the same password directory during init as it does during
   runtime (the change to getPassDir in initialization logic.
3. Prints significantly better errors if initialization fails
4. Has slightly cleaner abstractions by hiding the initialization check
   in 'runPass'

The 4th item there does mean there are a few cases where more work is
done before erroring, but that amount of work is trivial and my manual
audit didn't reveal anything that seemed worrying.

Fixes #96, alternative to #106

Signed-off-by: Euan Kemp <euank@euank.com>
2018-06-27 14:23:22 +02:00
Euan Kemp dd27c246bd pass: trim pass show output
As of 8446a40, pass show will include a newline when showing a password.
This causes the pass helper here to reliably fail to initialize since a
password doesn't round-trip.

Before making this change, the pass test would fail if the installed
password-store version was v1.7.1+, and after this change it passes
again.

Fixes #107

Signed-off-by: Euan Kemp <euank@euank.com>
2018-06-27 14:21:52 +02:00
Vincent Demeester 26deb2937d Merge pull request #109 from euank/better-exec
pass: simplify some code
2018-06-27 14:19:27 +02:00
Euan Kemp a13ff50017 pass: simplify some code
The exec.Command code and os.Getenv implementation were both needlessly
verbose. This replaces them with simpler variations.

Signed-off-by: Euan Kemp <euank@euank.com>
2018-06-27 14:15:36 +02:00
Vincent Demeester 1c295f7de8 Merge pull request #115 from n4ss/fix-appveyor
Fix Windows CI
2018-06-27 14:14:13 +02:00
Nassim 'Nass' Eddequiouaq 093af814ee fix go vet complaining on composite literales
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2018-06-27 14:02:28 +02:00
Vincent Demeester d499cf5cb9 Merge pull request #114 from n4ss/fix-travisci-osxkeychain
fix osxkeychain ci
2018-06-27 13:52:26 +02:00
Nassim 'Nass' Eddequiouaq b049338a6b Fix appveyor windows ci
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2018-06-27 13:45:32 +02:00
Nassim 'Nass' Eddequiouaq 91fc39d57a install yarn on osx for travisci
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2018-06-27 13:22:53 +02:00
Nassim 'Nass' Eddequiouaq 317219f3a6 Fix yarn complaint in travisci
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2018-06-27 12:13:18 +02:00
Nassim 'Nass' Eddequiouaq 21f4937ebc fix osxkeychain ci
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2018-06-27 11:58:22 +02:00
Vincent Demeester 19b711cc92 Merge pull request #100 from vdemeester/update-maintainers
Update MAINTAINERS file with current maintainers
2018-02-14 08:13:21 +01:00
Vincent Demeester 1f635a73ad Update MAINTAINERS file with current maintainers
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2018-02-12 14:39:43 +01:00
Piano Kang 302e4ae938 Typo fixed.
Signed-off-by: Piano Kang <kang@bridgewell.com>
2017-12-20 13:37:43 +08:00
Jean-Laurent de Morlhon d68f9aeca3 Merge pull request #88 from n4ss/add-pass-config-notice
Add instructions for pass helper configuration
2017-08-30 14:17:07 +02:00
Vincent Demeester 05a9d4c50d Merge pull request #89 from tych0/better-initialization-check
pass: better initialization check
2017-08-29 21:46:31 +02:00
Tycho Andersen c2eec534ee pass: better initialization check
See comment for details, but basically, let's actually use pass to check
that it works.

Signed-off-by: Tycho Andersen <tycho@docker.com>
2017-08-29 08:25:08 -07:00
Nassim 'Nass' Eddequiouaq 5be80ca212 Add instructions for pass helper configuration
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-08-28 18:11:27 +02:00
Vincent Demeester f00de1b72f Merge pull request #87 from n4ss/bump-0.6.0
Bump 0.6.0
2017-08-28 14:18:12 +02:00
Nassim 'Nass' Eddequiouaq 72f0375e37 Bump 0.6.0
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-08-25 19:37:31 +02:00
Nassim Eddequiouaq 3cce61446f Merge pull request #85 from promiseofcake/ljk-fix-makefile
Fix Makefile issues
2017-08-25 18:50:05 +02:00
Lucas Kacher ec0d036273 Fix Makefile issues
* mkdir fails when `bin` exists
* obtaining golint fails if previously installed (w/out the -u flag)

Signed-off-by: Lucas Kacher <lucas@vsco.co>
2017-08-23 11:08:54 -04:00
Vincent Demeester d3d9934897 Merge pull request #84 from tych0/fix-helpers-protocol
return "" instead of an error when pass isn't present
2017-08-22 20:10:46 +02:00
Tycho Andersen f212ea17df fix test
Signed-off-by: Tycho Andersen <tycho@docker.com>
2017-08-17 10:36:48 -06:00
Tycho Andersen 09e536a128 return "" instead of an error when pass isn't present
It turns out the cred helpers protocol is to return "" and not an error
when a credential isn't present, so let's do that.

Signed-off-by: Tycho Andersen <tycho@docker.com>
2017-08-17 09:19:19 -06:00
Vincent Demeester 3c90bd29a4 Merge pull request #83 from tych0/expose-pass-initialized
pass backend: expose pass initialized flag
2017-08-16 11:06:21 +02:00
Tycho Andersen b8fb9690c8 pass backend: expose pass initialized flag
This way clients don't have to run their own checks, they can just use the
library's detection.

Signed-off-by: Tycho Andersen <tycho@docker.com>
2017-08-14 08:56:59 -06:00
Vincent Demeester 7efaffb4c4 Merge pull request #81 from tych0/pass-backend-and-package
Add `pass` backend and debian packaging for both -secretservice and -pass backends
2017-08-14 16:00:25 +02:00
Nassim Eddequiouaq 6338c06ba4 Merge pull request #69 from shhsu/command_env
A new entry point for calling the cred helpers that allow passing environment variables
2017-08-14 11:15:13 +02:00
Tycho Andersen 86c94d3e30 add a deb package for pass/secret service backends
Note that this single source package produces two binary packages: one for
-pass, and one for -secretservice, so that users can install whichever
password backend (and thus deps) that they want.

Signed-off-by: Tycho Andersen <tycho@docker.com>
2017-08-11 08:15:29 -06:00
Tycho Andersen 1ab1037707 add a pass credential helper backend
Signed-off-by: Tycho Andersen <tycho@docker.com>
2017-08-11 08:15:29 -06:00
Peter Hsu c69c0725bb A new entry point for calling the cred helpers that allow passing environment variables
Signed-off-by: Peter Hsu <shhsu@microsoft.com>
2017-06-30 10:45:22 -07:00
Nassim Eddequiouaq a8de4f6e8a Merge pull request #77 from jeanlaurent/win-release
Adding winrelease target
2017-06-15 12:39:03 +02:00
Jean-Laurent de Morlhon 4fbc86d7d0 Adding winrelease target
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-06-15 12:03:00 +02:00
Vincent Demeester 2d19ebb7f4 Merge pull request #76 from thaJeztah/bump-0.5.2
Bump version 0.5.2
2017-06-15 11:31:05 +02:00
Sebastiaan van Stijn 054c53824f Bump version 0.5.2
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-06-15 11:21:41 +02:00
Sebastiaan van Stijn be1808e3ed Merge pull request #75 from thaJeztah/update-changelog
Update changelog
2017-06-15 11:07:09 +02:00
Sebastiaan van Stijn e1d4c012bc Fix storing URLs without scheme (#72)
* Fix storing URLs without scheme

If secrets are stored without specifying a scheme
(https://), the keychain-helper would interpret the
hostname as _path_, causing lookup of secrets to fail.

This patch makes sure that a scheme is added (if missing).

If no scheme is specified, https:// is used as a default.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

* Have pre go1.8 compiler able to compile

Signed-off-by: Tibor Vass <teabee89@gmail.com>

* Fix URL parsing with port and no scheme

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>

* Improve parseURL comment

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-06-15 10:58:17 +02:00
Sebastiaan van Stijn ad6ee5d58d Update changelog
The changelog wasn't updated for each release.

This patch updates the changelog, with changes
taken from the releases page

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-06-15 00:46:21 +02:00
Nassim Eddequiouaq 94be56b6f4 Merge pull request #74 from thaJeztah/bump-travis-to-go-18
Update TravisCI to use Go 1.8
2017-06-14 22:08:13 +02:00
Sebastiaan van Stijn 37bf8afe8b Update TravisCI to use Go 1.8
To match the version used in docker

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2017-06-14 21:29:13 +02:00
Nassim Eddequiouaq 3d7e1817f2 Merge pull request #73 from n4ss/travisci-golang1.7
Update TravisCI to use Go 1.7
2017-06-14 17:21:44 +02:00
Nassim 'Nass' Eddequiouaq 5651367281 Update TravisCI to use Go 1.7
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-06-14 16:18:01 +02:00
Nassim Eddequiouaq d8f57a18c6 Merge pull request #61 from danieljoos/linux32
Workaround for Linux 32-bit build
2017-05-30 20:56:52 +02:00
Sebastiaan van Stijn 4bc0bc2a85 Merge pull request #67 from jeanlaurent/add-version-command
Add version command
2017-05-30 19:13:34 +02:00
Jean-Laurent de Morlhon 167b137eb4 vet/lint/fmt before release
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-05-30 18:26:18 +02:00
Jean-Laurent de Morlhon 2a67ef1524 make linter happy
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-05-30 18:26:18 +02:00
Jean-Laurent de Morlhon 6ba4edf6e9 Read version from sources
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-05-30 18:26:18 +02:00
Jean-Laurent de Morlhon fa4a4d4f71 Bump Version 0.5.1-> 0.5.2-devel
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-05-30 18:26:18 +02:00
Jean-Laurent de Morlhon 4d60b372aa Add a version command
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-05-30 18:26:18 +02:00
Vincent Demeester 607bf3c174 Merge pull request #68 from jeanlaurent/appveyor-build
AppVeyor build
2017-05-30 08:05:06 -07:00
Jean-Laurent de Morlhon 74f4f75bc1 AppVeyor build
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-05-30 11:59:52 +02:00
Jean-Laurent de Morlhon 0e7779e5a1 Merge pull request #66 from n4ss/release-v0.5.1
Update version tag to v0.5.1
2017-05-29 16:44:20 +02:00
Nassim 'Nass' Eddequiouaq fb9549d396 Update version tag
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-05-29 16:32:21 +02:00
Nassim Eddequiouaq ed11c58ebf Prevent invalid credentials: no missing server URL or username (#62)
* Prevent invalid credentials: no missing server URL or username

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>

* Add missing username/serverURL error checks in credstore client

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>

* Clean up doc on invalid creds errors and client's error checks

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>

* Add tests for missing ServerURL/Username

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>

* Clean isValidCredsMessage prototype

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>

* Add test for missing server URL and more detailed error msg

Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-05-29 10:21:50 +02:00
Vincent Demeester 79ab7059b0 Merge pull request #64 from shhsu/redirect_stderr
Caller of cred helpers would now see the stderr returns from cred helpers
2017-05-26 09:25:21 -07:00
shhsu@microsoft.com 18d35e4984 Caller of cred helpers would now see the stderr returns from cred helpers
Signed-off-by: shhsu@microsoft.com <shhsu@microsoft.com>
2017-05-24 15:41:34 -07:00
Daniel Joos cdde659563 Workaround for Linux 32-bit build
This adds a workaround for the `secretservice` sub-package to build under 32bit Linux.
It limits the total number of list entries to (2^29)-1.
A short explanation of how this number was calculated has been added in a comment.

Signed-off-by: Daniel Joos <daniel@joosweb.de>
2017-05-10 00:38:33 +02:00
Vincent Demeester 71779cf7f5 Merge pull request #56 from simonferquel/align-get-with-list-filtering
[Windows] align get with list behavior
2017-03-22 09:53:49 +01:00
Simon Ferquel 51c78cdc14 [Windows] align get with list behavior
Avoiding to get unlabelled credentials

Signed-off-by: Simon Ferquel <simon.ferquel@docker.com>
2017-03-16 18:16:37 +01:00
Vincent Demeester 120e37f15d Merge pull request #54 from vdemeester/update-maintainers
Add n4ss as maintainer 👼
2017-03-14 19:30:38 +01:00
Vincent Demeester 4962f775bf Add n4ss as maintainer 👼
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2017-03-14 16:04:27 +01:00
Jean-Laurent de Morlhon ce617b3357 Merge pull request #52 from jeanlaurent/run-test-on-release
Clean and run test before osx release
2017-03-13 16:45:02 -07:00
Jean-Laurent de Morlhon f3071aff0a Merge pull request #53 from n4ss/fix-windows-typos-tests
Fix syntax typos and tests implementation for Windows
2017-03-13 16:44:48 -07:00
Nassim 'Nass' Eddequiouaq 1515d4547e Fix syntax typos and tests implementation for windows
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-13 14:51:03 -07:00
Jean-Laurent de Morlhon f67589c36e Clean and run test before osx release
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-03-13 12:49:05 -07:00
Vincent Demeester 479de2a4f5 Merge pull request #45 from jeanlaurent/fix-mkdir
Remove mkdir -p where neccessary
2017-03-13 20:43:59 +01:00
Vincent Demeester cdba2ced06 Merge pull request #44 from jeanlaurent/osxrelease-task
Add a osx release target to the makefile to ease releasing.
2017-03-13 20:43:49 +01:00
Nassim Eddequiouaq 7f0538cd5e Merge pull request #50 from n4ss/fix-secserv-tests
Fix secretservice tests and expected behaviors
2017-03-13 11:56:46 -07:00
Nassim Eddequiouaq 14381bf0d6 Merge pull request #49 from n4ss/label-creds
Label credentials on each platform' creds store and fix secretservice behavior
2017-03-13 11:22:23 -07:00
Nassim 'Nass' Eddequiouaq 7133af577e Creds label can be configured from helpers
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-11 12:34:22 +01:00
Nassim 'Nass' Eddequiouaq 2f2e85cfb9 Remove unnecessary creds list's size check
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 16:59:57 +01:00
Nassim 'Nass' Eddequiouaq 47566329ff Fix secretservice tests and expected behaviors
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 07:29:25 -08:00
Nassim 'Nass' Eddequiouaq b9d19b479a Return empty server-url to usernames map if no search results
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:23:43 +01:00
Nassim 'Nass' Eddequiouaq e522e56699 Fix memory leaks and non-null terminated strings usage
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:23:17 +01:00
Nassim 'Nass' Eddequiouaq 8cb3338668 Filter docker credentials with label directly through libsecret
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:18:14 +01:00
Nassim 'Nass' Eddequiouaq cd76e4253f Use the proper docker secret schema for items to search for
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:15:05 +01:00
Nassim 'Nass' Eddequiouaq 021d7d6a19 Add label filter on the list of secrets
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:14:34 +01:00
Nassim 'Nass' Eddequiouaq 2a8670e0da Cleanup original modifications to the exposed APIs
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:14:04 +01:00
Nassim 'Nass' Eddequiouaq c5fbd3a5ad Fix type conversion for labels added to search queries on macOS
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:12:53 +01:00
Nassim 'Nass' Eddequiouaq c6cf8aa13b Add Label to Credentials in test files
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:12:32 +01:00
Nassim 'Nass' Eddequiouaq cfe7556d6d [SYNTAX] Run gofmt on changed files
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:12:12 +01:00
Nassim 'Nass' Eddequiouaq 23a1f310a5 Add a Docker Credentials label support for windows
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:11:57 +01:00
Nassim 'Nass' Eddequiouaq f7f2744e6d Add a Docker Credentials label support for linux
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:11:25 +01:00
Nassim 'Nass' Eddequiouaq 406812bf8e Add a Docker Credentials label support for macOS
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:11:13 +01:00
Nassim 'Nass' Eddequiouaq 595b7f2531 Add a Docker Credentials label to store and list creds
Signed-off-by: Nassim 'Nass' Eddequiouaq <eddequiouaq.nassim@gmail.com>
2017-03-10 15:10:54 +01:00
Jean-Laurent de Morlhon ad4463616e Remove mkdir -p where neccessary
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-03-03 18:51:21 +01:00
Jean-Laurent de Morlhon 365da011fb Add a osx release target to the makefile to ease releasing.
Signed-off-by: Jean-Laurent de Morlhon <jeanlaurent@morlhon.net>
2017-03-03 18:49:23 +01:00
Jake Sanders 79f93e5e69 C.free(unsafe.Pointer(err)) -> defer C.g_error_free(err)
Signed-off-by: Jake Sanders <jsand@google.com>
2016-09-15 09:14:53 -07:00
61 changed files with 2216 additions and 421 deletions
+1
View File
@@ -1 +1,2 @@
bin bin
release
+5 -29
View File
@@ -3,46 +3,22 @@
sudo: required sudo: required
language: go language: go
dist: trusty dist: trusty
osx_image: xcode10.1
os: os:
- linux - linux
- osx - osx
notifications: notifications:
email: false email: false
go: go:
- 1.6 - 1.12.x
install: make deps
addons: addons:
apt: apt:
packages: packages:
- libsecret-1-dev - libsecret-1-dev
- pass
before_script: before_script:
- make deps
- "export DISPLAY=:99.0" - "export DISPLAY=:99.0"
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh ci/before_script_linux.sh; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh ci/before_script_linux.sh; fi
- make validate - make validate
script: make test script: make test
before_deploy:
- sh ci/before_deploy.sh
deploy:
provider: releases
api_key:
secure: "cGs5cao/MeVQVnum+Pr/Tpv+w83NsqGVS3wxvi3LYEf2ON4Kkmtd+Alwi0YFkGPJmSY0jZOct8NVK/M70qSnIU4l+AAq9+3KSMv23u4xrmy2sQog3AF+Ve3Rac+iYwZHOWwGs9I67CSuVv0vjJNVsDsTVefc25lHJImjRvXIS4p9xYzRPeUDCoqAo/QMVE+vFiMyxydsvt8fhd0gZCjPYWEpyHe9tjZ1tr1HsHZKFAjVb6AmF45d8rvadPoVUuLaOtr35wDC3XRKEvCZUefQpwLkrNj7j2L1rVGlY1xTE2APpLtvfd7R1Mx6kSfS1Gm3Pwcv3mugadXIhecL0lsdnU+BANjX3VUiv4ryzTPbsge966mv9ZQYwAzgCQTWRtMNJqsAnPZTeAkiOntd+HMQbPpxljOxv1sjDPY+EIZesyB3yQRJI8vMxqFcAjxeRyLcBqEnRFC2nd/Ln0KZ7ZFu16FcpNqRojdBayyypuXKqAiBNwtp4ti/65x8eHfBJuNjJtNZkRsJEYam4CYMRLxds9plKQfkaZ8045PKpyXO8fMpUhrfqSVID4IrYvD+io6XoXtdR4Lk6isZ2EgrjdrqgdG70S5lwKihL4iAi2F2ZCWhngFhkeNVOZunEWE6qZMk5wKODajR9sixGDApGPZQVojHwCNRGILZaHZ39JCIj3s="
# upload file artifacts using a glob expression.
# It requires both options `file_glob` and `file`:
# https://github.com/travis-ci/dpl/blob/master/lib/dpl/provider/releases.rb#L47-L53
file_glob: true
file: docker-credential-*-${TRAVIS_TAG}-amd64.tar.gz
# don't delete the artifacts from previous phases
skip_cleanup: true
# deploy when a new tag is pushed
on:
tags: true
branches:
only:
# Pushes and PR to the master branch
- master
# IMPORTANT Ruby regex to match tags. Required, or travis won't trigger deploys when a new tag
# is pushed. This regex matches semantic versions like v1.2.3-rc4+2016.02.22
- /^v\d+\.\d+\.\d+.*$/
+35 -1
View File
@@ -4,6 +4,41 @@ This changelog tracks the releases of docker-credential-helpers.
This project includes different binaries per platform. This project includes different binaries per platform.
The platform released is identified after the tag name. The platform released is identified after the tag name.
## v0.6.0 (Go client, Linux)
- New credential helper on Linux using `pass`
- New entry point for passing environment variables when calling a credential helper
- Add a Makefile rule generating a Windows release binary
### Note
`pass` needs to be configured for `docker-credential-pass` to work properly.
It must be initialized with a `gpg2` key ID. Make sure your GPG key exists is in `gpg2` keyring as `pass` uses `gpg2` instead of the regular `gpg`.
## v0.5.2 (Mac OS X, Windows, Linux)
- Add a `version` command to output the version
- Fix storing URLs without scheme, and use `https://` by default
## v0.5.1 (Go client, Mac OS X, Windows, Linux)
- Redirect credential helpers' standard error to the caller's
- Prevent invalid credentials and credentials queries
## v0.5.0 (Mac OS X)
- Add a label for Docker credentials and filter credentials lookup to filter keychain lookups
## v0.4.2 (Mac OS X, Windows)
- Fix osxkeychain list
- macOS binary is now signed on release
- Generate a `.exe` instead
## v0.4.1 (Mac OS X)
- Fixes to support older version of OSX (10.10, 10.11)
## v0.4.0 (Go client, Mac OS X, Windows, Linux) ## v0.4.0 (Go client, Mac OS X, Windows, Linux)
- Full implementation for OSX ready - Full implementation for OSX ready
@@ -11,7 +46,6 @@ The platform released is identified after the tag name.
- Implement client.List, change list API - Implement client.List, change list API
- mac: delete credentials before adding them to avoid already exist error (fixes #37) - mac: delete credentials before adding them to avoid already exist error (fixes #37)
## v0.3.0 (Go client) ## v0.3.0 (Go client)
- Add Go client library to talk with the native programs. - Add Go client library to talk with the native programs.
Vendored
+83
View File
@@ -0,0 +1,83 @@
pipeline {
agent none
options {
checkoutToSubdirectory('src/github.com/docker/docker-credential-helpers')
}
stages {
stage('build') {
parallel {
stage('linux') {
agent {
kubernetes {
label 'declarative'
containerTemplate {
name 'golang'
image 'golang:1.12.4'
ttyEnabled true
command 'cat'
}
}
}
environment {
GOPATH = pwd()
PATH = "/usr/local/go/bin:${GOPATH}/bin:$PATH"
}
steps {
container('golang') {
dir('src/github.com/docker/docker-credential-helpers') {
sh 'apt-get update && apt-get install -y libsecret-1-dev pass'
sh 'make deps fmt lint test'
sh 'make pass secretservice'
sh 'make linuxrelease'
archiveArtifacts 'release/docker-credential-*'
}
}
}
}
stage('mac') {
agent {
label 'mac-build && go1.12.4'
}
environment {
PATH = "/usr/local/go/bin:${GOPATH}/bin:$PATH"
GOPATH = pwd()
}
steps {
dir('src/github.com/docker/docker-credential-helpers') {
sh 'make deps fmt lint test'
sh 'make osxcodesign'
sh 'make osxrelease'
archiveArtifacts 'release/docker-credential-*'
}
}
}
stage('windows') {
agent {
label 'win-build && go1.12.4'
}
environment {
GOPATH = pwd()
PATH = "${pwd()}/bin;$PATH"
PFX = credentials('windows-build-2019-pfx')
PFXPASSWORD = credentials('windows-build-2019-pfx-password')
}
steps {
dir('src/github.com/docker/docker-credential-helpers') {
sh 'echo ${PFX} | base64 -d > pfx'
sh 'make deps fmt lint test'
sh 'make wincred'
bat """ "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x86\\signtool.exe" sign /fd SHA256 /a /f pfx /p ${PFXPASSWORD} /d Docker /du https://www.docker.com /t http://timestamp.verisign.com/scripts/timestamp.dll bin\\docker-credential-wincred.exe """
archiveArtifacts 'bin/docker-credential-*'
}
}
post {
always {
sh 'rm -f pfx'
}
}
}
}
}
}
}
+6 -30
View File
@@ -11,20 +11,16 @@
[Org] [Org]
[Org."Core maintainers"] [Org."Core maintainers"]
people = [ people = [
"aaronlehmann",
"calavera",
"coolljt0725", "coolljt0725",
"cpuguy83", "cpuguy83",
"crosbymichael", "crosbymichael",
"dnephin", "dnephin",
"dongluochen",
"duglin", "duglin",
"estesp", "estesp",
"icecrime",
"jhowardmsft", "jhowardmsft",
"lk4d4",
"mavenugo", "mavenugo",
"mhbauer", "mhbauer",
"n4ss",
"runcom", "runcom",
"stevvooe", "stevvooe",
"thajeztah", "thajeztah",
@@ -44,16 +40,6 @@
# ADD YOURSELF HERE IN ALPHABETICAL ORDER # ADD YOURSELF HERE IN ALPHABETICAL ORDER
[people.aaronlehmann]
Name = "Aaron Lehmann"
Email = "aaron.lehmann@docker.com"
GitHub = "aaronlehmann"
[people.calavera]
Name = "David Calavera"
Email = "david.calavera@gmail.com"
GitHub = "calavera"
[people.coolljt0725] [people.coolljt0725]
Name = "Lei Jitang" Name = "Lei Jitang"
Email = "leijitang@huawei.com" Email = "leijitang@huawei.com"
@@ -74,11 +60,6 @@
Email = "dnephin@gmail.com" Email = "dnephin@gmail.com"
GitHub = "dnephin" GitHub = "dnephin"
[people.dongluochen]
Name = "Dongluo Chen"
Email = "dongluo.chen@docker.com"
GitHub = "dongluochen"
[people.duglin] [people.duglin]
Name = "Doug Davis" Name = "Doug Davis"
Email = "dug@us.ibm.com" Email = "dug@us.ibm.com"
@@ -89,21 +70,11 @@
Email = "estesp@linux.vnet.ibm.com" Email = "estesp@linux.vnet.ibm.com"
GitHub = "estesp" GitHub = "estesp"
[people.icecrime]
Name = "Arnaud Porterie"
Email = "arnaud@docker.com"
GitHub = "icecrime"
[people.jhowardmsft] [people.jhowardmsft]
Name = "John Howard" Name = "John Howard"
Email = "jhoward@microsoft.com" Email = "jhoward@microsoft.com"
GitHub = "jhowardmsft" GitHub = "jhowardmsft"
[people.lk4d4]
Name = "Alexander Morozov"
Email = "lk4d4@docker.com"
GitHub = "lk4d4"
[people.mavenugo] [people.mavenugo]
Name = "Madhu Venugopal" Name = "Madhu Venugopal"
Email = "madhu@docker.com" Email = "madhu@docker.com"
@@ -114,6 +85,11 @@
Email = "mbauer@us.ibm.com" Email = "mbauer@us.ibm.com"
GitHub = "mhbauer" GitHub = "mhbauer"
[people.n4ss]
Name = "Nassim Eddequiouaq"
Email = "eddequiouaq.nassim@gmail.com"
GitHub = "n4ss"
[people.runcom] [people.runcom]
Name = "Antonio Murdaca" Name = "Antonio Murdaca"
Email = "runcom@redhat.com" Email = "runcom@redhat.com"
+45 -4
View File
@@ -1,17 +1,22 @@
.PHONY: all deps osxkeychain secretservice test validate wincred .PHONY: all deps osxkeychain secretservice test validate wincred pass deb
TRAVIS_OS_NAME ?= linux TRAVIS_OS_NAME ?= linux
VERSION := $(shell grep 'const Version' credentials/version.go | awk -F'"' '{ print $$2 }')
all: test all: test
deps: deps:
go get github.com/golang/lint/golint go get -u golang.org/x/lint/golint
clean:
rm -rf bin
rm -rf release
osxkeychain: osxkeychain:
mkdir -p bin mkdir -p bin
go build -ldflags -s -o bin/docker-credential-osxkeychain osxkeychain/cmd/main_darwin.go go build -ldflags -s -o bin/docker-credential-osxkeychain osxkeychain/cmd/main_darwin.go
codesign: osxkeychain osxcodesign: osxkeychain
$(eval SIGNINGHASH = $(shell security find-identity -v -p codesigning | grep "Developer ID Application: Docker Inc" | cut -d ' ' -f 4)) $(eval SIGNINGHASH = $(shell security find-identity -v -p codesigning | grep "Developer ID Application: Docker Inc" | cut -d ' ' -f 4))
xcrun -log codesign -s $(SIGNINGHASH) --force --verbose bin/docker-credential-osxkeychain xcrun -log codesign -s $(SIGNINGHASH) --force --verbose bin/docker-credential-osxkeychain
xcrun codesign --verify --deep --strict --verbose=2 --display bin/docker-credential-osxkeychain xcrun codesign --verify --deep --strict --verbose=2 --display bin/docker-credential-osxkeychain
@@ -20,10 +25,27 @@ secretservice:
mkdir -p bin mkdir -p bin
go build -o bin/docker-credential-secretservice secretservice/cmd/main_linux.go go build -o bin/docker-credential-secretservice secretservice/cmd/main_linux.go
pass:
mkdir -p bin
go build -o bin/docker-credential-pass pass/cmd/main.go
wincred: wincred:
mkdir -p bin mkdir -p bin
go build -o bin/docker-credential-wincred.exe wincred/cmd/main_windows.go go build -o bin/docker-credential-wincred.exe wincred/cmd/main_windows.go
linuxrelease:
mkdir -p release
cd bin && tar cvfz ../release/docker-credential-pass-v$(VERSION)-amd64.tar.gz docker-credential-pass
cd bin && tar cvfz ../release/docker-credential-secretservice-v$(VERSION)-amd64.tar.gz docker-credential-secretservice
osxrelease:
mkdir -p release
cd bin && tar cvfz ../release/docker-credential-osxkeychain-v$(VERSION)-amd64.tar.gz docker-credential-osxkeychain
winrelease:
mkdir -p release
cd bin && zip ../release/docker-credential-wincred-v$(VERSION)-amd64.zip docker-credential-wincred.exe
test: test:
# tests all packages except vendor # tests all packages except vendor
go test -v `go list ./... | grep -v /vendor/` go test -v `go list ./... | grep -v /vendor/`
@@ -31,14 +53,33 @@ test:
vet: vet_$(TRAVIS_OS_NAME) vet: vet_$(TRAVIS_OS_NAME)
go vet ./credentials go vet ./credentials
vet_win:
go vet ./wincred
vet_osx: vet_osx:
go vet ./osxkeychain go vet ./osxkeychain
vet_linux: vet_linux:
go vet ./secretservice go vet ./secretservice
validate: vet lint:
for p in `go list ./... | grep -v /vendor/`; do \ for p in `go list ./... | grep -v /vendor/`; do \
golint $$p ; \ golint $$p ; \
done done
fmt:
gofmt -s -l `ls **/*.go | grep -v vendor` gofmt -s -l `ls **/*.go | grep -v vendor`
validate: vet lint fmt
BUILDIMG:=docker-credential-secretservice-$(VERSION)
deb:
mkdir -p release
docker build -f deb/Dockerfile \
--build-arg VERSION=$(VERSION) \
--build-arg DISTRO=xenial \
--tag $(BUILDIMG) \
.
docker run --rm --net=none $(BUILDIMG) tar cf - /release | tar xf -
docker rmi $(BUILDIMG)
+7 -1
View File
@@ -16,7 +16,7 @@ The programs in this repository are written with the Go programming language. Th
$ go get github.com/docker/docker-credential-helpers $ go get github.com/docker/docker-credential-helpers
``` ```
2 - Use `make` to build the program you want. That will leave any executable in the `bin` directory inside the repository. 2 - Use `make` to build the program you want. That will leave an executable in the `bin` directory inside the repository.
``` ```
$ cd $GOPATH/docker/docker-credentials-helpers $ cd $GOPATH/docker/docker-credentials-helpers
@@ -55,6 +55,12 @@ You can see examples of each function in the [client](https://godoc.org/github.c
1. osxkeychain: Provides a helper to use the OS X keychain as credentials store. 1. osxkeychain: Provides a helper to use the OS X keychain as credentials store.
2. secretservice: Provides a helper to use the D-Bus secret service as credentials store. 2. secretservice: Provides a helper to use the D-Bus secret service as credentials store.
3. wincred: Provides a helper to use Windows credentials manager as store. 3. wincred: Provides a helper to use Windows credentials manager as store.
4. pass: Provides a helper to use `pass` as credentials store.
#### Note
`pass` needs to be configured for `docker-credential-pass` to work properly.
It must be initialized with a `gpg2` key ID. Make sure your GPG key exists is in `gpg2` keyring as `pass` uses `gpg2` instead of the regular `gpg`.
## Development ## Development
-67
View File
@@ -1,67 +0,0 @@
version: "{build}"
# Source Config
clone_folder: c:\gopath\src\github.com\docker\docker-credential-helpers
# Build host
environment:
global:
GOPATH: c:\gopath
CGO_ENABLED: 1
GOVERSION: 1.6
matrix:
- platform: x86
GOARCH: 386
MSYS2_BITS: 32
- platform: x64
GOARCH: amd64
MSYS2_BITS: 64
init:
- git config --global core.autocrlf input
# Build
install:
# Install Go 1.6.
- rmdir c:\go /s /q
- appveyor DownloadFile https://storage.googleapis.com/golang/go%GOVERSION%.windows-%GOARCH%.msi
- msiexec /i go%GOVERSION%.windows-%GOARCH%.msi /q
- set Path=c:\msys64\mingw%MSYS2_BITS%\bin;c:\go\bin;%Path%
- go version
- go env
build: false
test_script:
- go vet ./wincred
- go test -v github.com/docker/docker-credential-helpers/wincred
# Equivalent to `before_deploy` phase
after_test:
# build binary
- mkdir bin
- go build -o bin/docker-credential-wincred wincred/cmd/main_windows.go
# build zipfile, will look like docker-credential-wincred-v0.1.0-amd64.zip in the root directory
- cd bin && 7z a ../docker-credential-wincred-%APPVEYOR_REPO_TAG_NAME%-%GOARCH%.zip docker-credential-wincred
# IMPORTANT All the artifacts need to be listed here, or they won't be uploaded to GitHub
artifacts:
- path: docker-credential-wincred-$(APPVEYOR_REPO_TAG_NAME)-$(GOARCH).zip
name: docker-credential-wincred-$(APPVEYOR_REPO_TAG_NAME)-$(GOARCH).zip
deploy:
# All the zipped artifacts will be deployed
description: "Visit the [Changelog](https://github.com/docker/docker-credential-helpers/blob/master/CHANGELOG.md) for a detailed description of what's new in this release."
artifact: /.*\.zip/
auth_token:
secure: ixWmTXZs8aV5+9s6vPXziIcdMMLd+lBVINJ0K/Sy++2wllpRxUec4/TPVKUGLqvL
provider: GitHub
# deploy when a new tag is pushed
on:
appveyor_repo_tag: true
branches:
only:
- master
-15
View File
@@ -1,15 +0,0 @@
set -ex
mkdir bin
case "$TRAVIS_OS_NAME" in
"osx")
go build -o bin/docker-credential-osxkeychain osxkeychain/cmd/main_darwin.go
cd bin
tar czf ../docker-credential-osxkeychain-${TRAVIS_TAG}-amd64.tar.gz docker-credential-osxkeychain
;;
"linux")
go build -o bin/docker-credential-secretservice secretservice/cmd/main_linux.go
cd bin
tar czf ../docker-credential-secretservice-${TRAVIS_TAG}-amd64.tar.gz docker-credential-secretservice
;;
esac
+18
View File
@@ -2,3 +2,21 @@ set -ex
sh -e /etc/init.d/xvfb start sh -e /etc/init.d/xvfb start
sleep 3 # give xvfb some time to start sleep 3 # give xvfb some time to start
# init key for pass
gpg --batch --gen-key <<-EOF
%echo Generating a standard key
Key-Type: DSA
Key-Length: 1024
Subkey-Type: ELG-E
Subkey-Length: 1024
Name-Real: Meshuggah Rocks
Name-Email: meshuggah@example.com
Expire-Date: 0
# Do a commit here, so that we can later print "done" :-)
%commit
%echo done
EOF
key=$(gpg --no-auto-check-trustdb --list-secret-keys | grep ^sec | cut -d/ -f2 | cut -d" " -f1)
pass init $key
+36 -2
View File
@@ -9,12 +9,27 @@ import (
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
) )
// isValidCredsMessage checks if 'msg' contains invalid credentials error message.
// It returns whether the logs are free of invalid credentials errors and the error if it isn't.
// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername.
func isValidCredsMessage(msg string) error {
if credentials.IsCredentialsMissingServerURLMessage(msg) {
return credentials.NewErrCredentialsMissingServerURL()
}
if credentials.IsCredentialsMissingUsernameMessage(msg) {
return credentials.NewErrCredentialsMissingUsername()
}
return nil
}
// Store uses an external program to save credentials. // Store uses an external program to save credentials.
func Store(program ProgramFunc, credentials *credentials.Credentials) error { func Store(program ProgramFunc, creds *credentials.Credentials) error {
cmd := program("store") cmd := program("store")
buffer := new(bytes.Buffer) buffer := new(bytes.Buffer)
if err := json.NewEncoder(buffer).Encode(credentials); err != nil { if err := json.NewEncoder(buffer).Encode(creds); err != nil {
return err return err
} }
cmd.Input(buffer) cmd.Input(buffer)
@@ -22,6 +37,11 @@ func Store(program ProgramFunc, credentials *credentials.Credentials) error {
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
t := strings.TrimSpace(string(out)) t := strings.TrimSpace(string(out))
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, t) return fmt.Errorf("error storing credentials - err: %v, out: `%s`", err, t)
} }
@@ -41,6 +61,10 @@ func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error
return nil, credentials.NewErrCredentialsNotFound() return nil, credentials.NewErrCredentialsNotFound()
} }
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, t) return nil, fmt.Errorf("error getting credentials - err: %v, out: `%s`", err, t)
} }
@@ -62,6 +86,11 @@ func Erase(program ProgramFunc, serverURL string) error {
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
t := strings.TrimSpace(string(out)) t := strings.TrimSpace(string(out))
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t) return fmt.Errorf("error erasing credentials - err: %v, out: `%s`", err, t)
} }
@@ -75,6 +104,11 @@ func List(program ProgramFunc) (map[string]string, error) {
out, err := cmd.Output() out, err := cmd.Output()
if err != nil { if err != nil {
t := strings.TrimSpace(string(out)) t := strings.TrimSpace(string(out))
if isValidErr := isValidCredsMessage(t); isValidErr != nil {
err = isValidErr
}
return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t) return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t)
} }
+6
View File
@@ -56,6 +56,8 @@ func (m *mockProgram) Output() ([]byte, error) {
return []byte(credentials.NewErrCredentialsNotFound().Error()), errProgramExited return []byte(credentials.NewErrCredentialsNotFound().Error()), errProgramExited
case invalidServerAddress: case invalidServerAddress:
return []byte("program failed"), errProgramExited return []byte("program failed"), errProgramExited
case "":
return []byte(credentials.NewErrCredentialsMissingServerURL().Error()), errProgramExited
} }
case "store": case "store":
var c credentials.Credentials var c credentials.Credentials
@@ -158,12 +160,16 @@ func TestGet(t *testing.T) {
} }
} }
missingServerURLErr := credentials.NewErrCredentialsMissingServerURL()
invalid := []struct { invalid := []struct {
serverURL string serverURL string
err string err string
}{ }{
{missingCredsAddress, credentials.NewErrCredentialsNotFound().Error()}, {missingCredsAddress, credentials.NewErrCredentialsNotFound().Error()},
{invalidServerAddress, "error getting credentials - err: exited 1, out: `program failed`"}, {invalidServerAddress, "error getting credentials - err: exited 1, out: `program failed`"},
{"", fmt.Sprintf("error getting credentials - err: %s, out: `%s`",
missingServerURLErr.Error(), missingServerURLErr.Error())},
} }
for _, v := range invalid { for _, v := range invalid {
+22 -2
View File
@@ -1,8 +1,11 @@
package client package client
import ( import (
"fmt"
"io" "io"
"os/exec" "os"
exec "golang.org/x/sys/execabs"
) )
// Program is an interface to execute external programs. // Program is an interface to execute external programs.
@@ -16,11 +19,28 @@ type ProgramFunc func(args ...string) Program
// NewShellProgramFunc creates programs that are executed in a Shell. // NewShellProgramFunc creates programs that are executed in a Shell.
func NewShellProgramFunc(name string) ProgramFunc { func NewShellProgramFunc(name string) ProgramFunc {
return NewShellProgramFuncWithEnv(name, nil)
}
// NewShellProgramFuncWithEnv creates programs that are executed in a Shell with environment variables
func NewShellProgramFuncWithEnv(name string, env *map[string]string) ProgramFunc {
return func(args ...string) Program { return func(args ...string) Program {
return &Shell{cmd: exec.Command(name, args...)} return &Shell{cmd: createProgramCmdRedirectErr(name, args, env)}
} }
} }
func createProgramCmdRedirectErr(commandName string, args []string, env *map[string]string) *exec.Cmd {
programCmd := exec.Command(commandName, args...)
programCmd.Env = os.Environ()
if env != nil {
for k, v := range *env {
programCmd.Env = append(programCmd.Env, fmt.Sprintf("%s=%s", k, v))
}
}
programCmd.Stderr = os.Stderr
return programCmd
}
// Shell invokes shell commands to talk with a remote credentials helper. // Shell invokes shell commands to talk with a remote credentials helper.
type Shell struct { type Shell struct {
cmd *exec.Cmd cmd *exec.Cmd
+48 -3
View File
@@ -17,6 +17,32 @@ type Credentials struct {
Secret string Secret string
} }
// isValid checks the integrity of Credentials object such that no credentials lack
// a server URL or a username.
// It returns whether the credentials are valid and the error if it isn't.
// error values can be errCredentialsMissingServerURL or errCredentialsMissingUsername
func (c *Credentials) isValid() (bool, error) {
if len(c.ServerURL) == 0 {
return false, NewErrCredentialsMissingServerURL()
}
if len(c.Username) == 0 {
return false, NewErrCredentialsMissingUsername()
}
return true, nil
}
// CredsLabel holds the way Docker credentials should be labeled as such in credentials stores that allow labelling.
// That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain,
// Windows credentials manager and Linux libsecret. Default value is "Docker Credentials"
var CredsLabel = "Docker Credentials"
// SetCredsLabel is a simple setter for CredsLabel
func SetCredsLabel(label string) {
CredsLabel = label
}
// Serve initializes the credentials helper and parses the action argument. // Serve initializes the credentials helper and parses the action argument.
// This function is designed to be called from a command line interface. // This function is designed to be called from a command line interface.
// It uses os.Args[1] as the key for the action. // It uses os.Args[1] as the key for the action.
@@ -25,7 +51,7 @@ type Credentials struct {
func Serve(helper Helper) { func Serve(helper Helper) {
var err error var err error
if len(os.Args) != 2 { if len(os.Args) != 2 {
err = fmt.Errorf("Usage: %s <store|get|erase|list>", os.Args[0]) err = fmt.Errorf("Usage: %s <store|get|erase|list|version>", os.Args[0])
} }
if err == nil { if err == nil {
@@ -49,6 +75,8 @@ func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error
return Erase(helper, in) return Erase(helper, in)
case "list": case "list":
return List(helper, out) return List(helper, out)
case "version":
return PrintVersion(out)
} }
return fmt.Errorf("Unknown credential action `%s`", key) return fmt.Errorf("Unknown credential action `%s`", key)
} }
@@ -72,6 +100,10 @@ func Store(helper Helper, reader io.Reader) error {
return err return err
} }
if ok, err := creds.isValid(); !ok {
return err
}
return helper.Add(&creds) return helper.Add(&creds)
} }
@@ -91,6 +123,9 @@ func Get(helper Helper, reader io.Reader, writer io.Writer) error {
} }
serverURL := strings.TrimSpace(buffer.String()) serverURL := strings.TrimSpace(buffer.String())
if len(serverURL) == 0 {
return NewErrCredentialsMissingServerURL()
}
username, secret, err := helper.Get(serverURL) username, secret, err := helper.Get(serverURL)
if err != nil { if err != nil {
@@ -98,8 +133,9 @@ func Get(helper Helper, reader io.Reader, writer io.Writer) error {
} }
resp := Credentials{ resp := Credentials{
Username: username, ServerURL: serverURL,
Secret: secret, Username: username,
Secret: secret,
} }
buffer.Reset() buffer.Reset()
@@ -126,6 +162,9 @@ func Erase(helper Helper, reader io.Reader) error {
} }
serverURL := strings.TrimSpace(buffer.String()) serverURL := strings.TrimSpace(buffer.String())
if len(serverURL) == 0 {
return NewErrCredentialsMissingServerURL()
}
return helper.Delete(serverURL) return helper.Delete(serverURL)
} }
@@ -139,3 +178,9 @@ func List(helper Helper, writer io.Writer) error {
} }
return json.NewEncoder(writer).Encode(accts) return json.NewEncoder(writer).Encode(accts)
} }
//PrintVersion outputs the current version.
func PrintVersion(writer io.Writer) error {
fmt.Fprintln(writer, Version)
return nil
}
+90
View File
@@ -73,6 +73,46 @@ func TestStore(t *testing.T) {
} }
} }
func TestStoreMissingServerURL(t *testing.T) {
creds := &Credentials{
ServerURL: "",
Username: "foo",
Secret: "bar",
}
b, err := json.Marshal(creds)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(b)
h := newMemoryStore()
if err := Store(h, in); IsCredentialsMissingServerURL(err) == false {
t.Fatal(err)
}
}
func TestStoreMissingUsername(t *testing.T) {
creds := &Credentials{
ServerURL: "https://index.docker.io/v1/",
Username: "",
Secret: "bar",
}
b, err := json.Marshal(creds)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(b)
h := newMemoryStore()
if err := Store(h, in); IsCredentialsMissingUsername(err) == false {
t.Fatal(err)
}
}
func TestGet(t *testing.T) { func TestGet(t *testing.T) {
serverURL := "https://index.docker.io/v1/" serverURL := "https://index.docker.io/v1/"
creds := &Credentials{ creds := &Credentials{
@@ -115,6 +155,32 @@ func TestGet(t *testing.T) {
} }
} }
func TestGetMissingServerURL(t *testing.T) {
serverURL := "https://index.docker.io/v1/"
creds := &Credentials{
ServerURL: serverURL,
Username: "foo",
Secret: "bar",
}
b, err := json.Marshal(creds)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(b)
h := newMemoryStore()
if err := Store(h, in); err != nil {
t.Fatal(err)
}
buf := strings.NewReader("")
w := new(bytes.Buffer)
if err := Get(h, buf, w); IsCredentialsMissingServerURL(err) == false {
t.Fatal(err)
}
}
func TestErase(t *testing.T) { func TestErase(t *testing.T) {
serverURL := "https://index.docker.io/v1/" serverURL := "https://index.docker.io/v1/"
creds := &Credentials{ creds := &Credentials{
@@ -144,6 +210,30 @@ func TestErase(t *testing.T) {
} }
} }
func TestEraseMissingServerURL(t *testing.T) {
serverURL := "https://index.docker.io/v1/"
creds := &Credentials{
ServerURL: serverURL,
Username: "foo",
Secret: "bar",
}
b, err := json.Marshal(creds)
if err != nil {
t.Fatal(err)
}
in := bytes.NewReader(b)
h := newMemoryStore()
if err := Store(h, in); err != nil {
t.Fatal(err)
}
buf := strings.NewReader("")
if err := Erase(h, buf); IsCredentialsMissingServerURL(err) == false {
t.Fatal(err)
}
}
func TestList(t *testing.T) { func TestList(t *testing.T) {
//This tests that there is proper input an output into the byte stream //This tests that there is proper input an output into the byte stream
//Individual stores are very OS specific and have been tested in osxkeychain and secretservice respectively //Individual stores are very OS specific and have been tested in osxkeychain and secretservice respectively
+68 -3
View File
@@ -1,8 +1,15 @@
package credentials package credentials
// ErrCredentialsNotFound standarizes the not found error, so every helper returns const (
// the same message and docker can handle it properly. // ErrCredentialsNotFound standardizes the not found error, so every helper returns
const errCredentialsNotFoundMessage = "credentials not found in native keychain" // the same message and docker can handle it properly.
errCredentialsNotFoundMessage = "credentials not found in native keychain"
// ErrCredentialsMissingServerURL and ErrCredentialsMissingUsername standardize
// invalid credentials or credentials management operations
errCredentialsMissingServerURLMessage = "no credentials server URL"
errCredentialsMissingUsernameMessage = "no credentials username"
)
// errCredentialsNotFound represents an error // errCredentialsNotFound represents an error
// raised when credentials are not in the store. // raised when credentials are not in the store.
@@ -35,3 +42,61 @@ func IsErrCredentialsNotFound(err error) bool {
func IsErrCredentialsNotFoundMessage(err string) bool { func IsErrCredentialsNotFoundMessage(err string) bool {
return err == errCredentialsNotFoundMessage return err == errCredentialsNotFoundMessage
} }
// errCredentialsMissingServerURL represents an error raised
// when the credentials object has no server URL or when no
// server URL is provided to a credentials operation requiring
// one.
type errCredentialsMissingServerURL struct{}
func (errCredentialsMissingServerURL) Error() string {
return errCredentialsMissingServerURLMessage
}
// errCredentialsMissingUsername represents an error raised
// when the credentials object has no username or when no
// username is provided to a credentials operation requiring
// one.
type errCredentialsMissingUsername struct{}
func (errCredentialsMissingUsername) Error() string {
return errCredentialsMissingUsernameMessage
}
// NewErrCredentialsMissingServerURL creates a new error for
// errCredentialsMissingServerURL.
func NewErrCredentialsMissingServerURL() error {
return errCredentialsMissingServerURL{}
}
// NewErrCredentialsMissingUsername creates a new error for
// errCredentialsMissingUsername.
func NewErrCredentialsMissingUsername() error {
return errCredentialsMissingUsername{}
}
// IsCredentialsMissingServerURL returns true if the error
// was an errCredentialsMissingServerURL.
func IsCredentialsMissingServerURL(err error) bool {
_, ok := err.(errCredentialsMissingServerURL)
return ok
}
// IsCredentialsMissingServerURLMessage checks for an
// errCredentialsMissingServerURL in the error message.
func IsCredentialsMissingServerURLMessage(err string) bool {
return err == errCredentialsMissingServerURLMessage
}
// IsCredentialsMissingUsername returns true if the error
// was an errCredentialsMissingUsername.
func IsCredentialsMissingUsername(err error) bool {
_, ok := err.(errCredentialsMissingUsername)
return ok
}
// IsCredentialsMissingUsernameMessage checks for an
// errCredentialsMissingUsername in the error message.
func IsCredentialsMissingUsernameMessage(err string) bool {
return err == errCredentialsMissingUsernameMessage
}
+4
View File
@@ -0,0 +1,4 @@
package credentials
// Version holds a string describing the current version
const Version = "0.6.4"
+19
View File
@@ -0,0 +1,19 @@
FROM ubuntu:xenial
ARG VERSION
ARG DISTRO
RUN apt-get update && apt-get install -yy debhelper dh-make golang-go libsecret-1-dev
RUN mkdir -p /build
WORKDIR /build
ENV GOPATH /build
COPY Makefile .
COPY credentials credentials
COPY secretservice secretservice
COPY pass pass
COPY deb/debian ./debian
COPY deb/build-deb .
RUN /build/build-deb ${VERSION} ${DISTRO}
Executable
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -x
set -e
version=$1
distro=$2
maintainer=$(awk -F ': ' '$1 == "Maintainer" { print $2; exit }' debian/control)
cat > "debian/changelog" <<-EOF
docker-credential-helpers ($version) $DISTRO; urgency=low
* New upstream version
-- $maintainer $(date --rfc-2822)
EOF
mkdir -p src/github.com/docker/docker-credential-helpers
ln -s /build/credentials /build/src/github.com/docker/docker-credential-helpers/credentials
ln -s /build/secretservice /build/src/github.com/docker/docker-credential-helpers/secretservice
ln -s /build/pass /build/src/github.com/docker/docker-credential-helpers/pass
dpkg-buildpackage -us -uc
mkdir /release
mv /docker-credential-* /release
+1
View File
@@ -0,0 +1 @@
9
+25
View File
@@ -0,0 +1,25 @@
Source: docker-credential-helpers
Section: admin
Priority: optional
Maintainer: Docker <support@docker.com>
Homepage: https://dockerproject.org
Standards-Version: 3.9.6
Vcs-Browser: https://github.com/docker/docker-credential-helpers
Vcs-Git: git://github.com/docker/docker-credential-helpers.git
Build-Depends: debhelper
, dh-make
, libsecret-1-dev
Package: docker-credential-secretservice
Architecture: any
Depends: libsecret-1-0
, ${misc:Depends}
Description: docker-credential-secretservice is a credential helper backend
which uses libsecret to keep Docker credentials safe.
Package: docker-credential-pass
Architecture: any
Depends: pass
, ${misc:Depends}
Description: docker-credential-secretservice is a credential helper backend
which uses the pass utility to keep Docker credentials safe.
@@ -0,0 +1 @@
debian/tmp/usr/bin/docker-credential-pass
@@ -0,0 +1 @@
debian/tmp/usr/bin/docker-credential-secretservice
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/make -f
DESTDIR := $(CURDIR)/debian/tmp
override_dh_auto_build:
make secretservice pass
override_dh_auto_install:
install -D bin/docker-credential-secretservice $(DESTDIR)/usr/bin/docker-credential-secretservice
install -D bin/docker-credential-pass $(DESTDIR)/usr/bin/docker-credential-pass
%:
dh $@
override_dh_auto_test:
# no tests
+8
View File
@@ -0,0 +1,8 @@
module github.com/docker/docker-credential-helpers
go 1.13
require (
github.com/danieljoos/wincred v1.1.0
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4
)
+16
View File
@@ -0,0 +1,16 @@
github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
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/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/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+26 -4
View File
@@ -14,7 +14,9 @@ char *get_error(OSStatus status) {
return buf; return buf;
} }
char *keychain_add(struct Server *server, char *username, char *secret) { char *keychain_add(struct Server *server, char *label, char *username, char *secret) {
SecKeychainItemRef item;
OSStatus status = SecKeychainAddInternetPassword( OSStatus status = SecKeychainAddInternetPassword(
NULL, NULL,
strlen(server->host), server->host, strlen(server->host), server->host,
@@ -25,11 +27,27 @@ char *keychain_add(struct Server *server, char *username, char *secret) {
server->proto, server->proto,
kSecAuthenticationTypeDefault, kSecAuthenticationTypeDefault,
strlen(secret), secret, strlen(secret), secret,
NULL &item
); );
if (status) { if (status) {
return get_error(status); return get_error(status);
} }
SecKeychainAttribute attribute;
SecKeychainAttributeList attrs;
attribute.tag = kSecLabelItemAttr;
attribute.data = label;
attribute.length = strlen(label);
attrs.count = 1;
attrs.attr = &attribute;
status = SecKeychainItemModifyContent(item, &attrs, 0, NULL);
if (status) {
return get_error(status);
}
return NULL; return NULL;
} }
@@ -116,16 +134,21 @@ char * CFStringToCharArr(CFStringRef aString) {
return NULL; return NULL;
} }
char *keychain_list(char *** paths, char *** accts, unsigned int *list_l) { char *keychain_list(char *credsLabel, char *** paths, char *** accts, unsigned int *list_l) {
CFStringRef credsLabelCF = CFStringCreateWithCString(NULL, credsLabel, kCFStringEncodingUTF8);
CFMutableDictionaryRef query = CFDictionaryCreateMutable (NULL, 1, NULL, NULL); CFMutableDictionaryRef query = CFDictionaryCreateMutable (NULL, 1, NULL, NULL);
CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword); CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword);
CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue); CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue);
CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll); CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll);
CFDictionaryAddValue(query, kSecAttrLabel, credsLabelCF);
//Use this query dictionary //Use this query dictionary
CFTypeRef result= NULL; CFTypeRef result= NULL;
OSStatus status = SecItemCopyMatching( OSStatus status = SecItemCopyMatching(
query, query,
&result); &result);
CFRelease(credsLabelCF);
//Ran a search and store the results in result //Ran a search and store the results in result
if (status) { if (status) {
return get_error(status); return get_error(status);
@@ -201,5 +224,4 @@ void freeListData(char *** data, unsigned int length) {
for(int i=0; i<length; i++) { for(int i=0; i<length; i++) {
free((*data)[i]); free((*data)[i]);
} }
free(*data);
} }
+38 -21
View File
@@ -1,8 +1,8 @@
package osxkeychain package osxkeychain
/* /*
#cgo CFLAGS: -x objective-c -mmacosx-version-min=10.10 #cgo CFLAGS: -x objective-c -mmacosx-version-min=10.11
#cgo LDFLAGS: -framework Security -framework Foundation -mmacosx-version-min=10.10 #cgo LDFLAGS: -framework Security -framework Foundation -mmacosx-version-min=10.11
#include "osxkeychain_darwin.h" #include "osxkeychain_darwin.h"
#include <stdlib.h> #include <stdlib.h>
@@ -10,18 +10,24 @@ package osxkeychain
import "C" import "C"
import ( import (
"errors" "errors"
"net/url"
"strconv" "strconv"
"strings"
"unsafe" "unsafe"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/registryurl"
) )
// errCredentialsNotFound is the specific error message returned by OS X // errCredentialsNotFound is the specific error message returned by OS X
// when the credentials are not in the keychain. // when the credentials are not in the keychain.
const errCredentialsNotFound = "The specified item could not be found in the keychain." const errCredentialsNotFound = "The specified item could not be found in the keychain."
// errCredentialsNotFound is the specific error message returned by OS X
// when environment does not allow showing dialog to unlock keychain.
const errInteractionNotAllowed = "User interaction is not allowed."
// ErrInteractionNotAllowed is returned if keychain password prompt can not be shown.
var ErrInteractionNotAllowed = errors.New(`keychain cannot be accessed because the current session does not allow user interaction. The keychain may be locked; unlock it by running "security -v unlock-keychain ~/Library/Keychains/login.keychain-db" and try again`)
// Osxkeychain handles secrets using the OS X Keychain as store. // Osxkeychain handles secrets using the OS X Keychain as store.
type Osxkeychain struct{} type Osxkeychain struct{}
@@ -35,12 +41,14 @@ func (h Osxkeychain) Add(creds *credentials.Credentials) error {
} }
defer freeServer(s) defer freeServer(s)
label := C.CString(credentials.CredsLabel)
defer C.free(unsafe.Pointer(label))
username := C.CString(creds.Username) username := C.CString(creds.Username)
defer C.free(unsafe.Pointer(username)) defer C.free(unsafe.Pointer(username))
secret := C.CString(creds.Secret) secret := C.CString(creds.Secret)
defer C.free(unsafe.Pointer(secret)) defer C.free(unsafe.Pointer(secret))
errMsg := C.keychain_add(s, username, secret) errMsg := C.keychain_add(s, label, username, secret)
if errMsg != nil { if errMsg != nil {
defer C.free(unsafe.Pointer(errMsg)) defer C.free(unsafe.Pointer(errMsg))
return errors.New(C.GoString(errMsg)) return errors.New(C.GoString(errMsg))
@@ -88,6 +96,9 @@ func (h Osxkeychain) Get(serverURL string) (string, string, error) {
if goMsg == errCredentialsNotFound { if goMsg == errCredentialsNotFound {
return "", "", credentials.NewErrCredentialsNotFound() return "", "", credentials.NewErrCredentialsNotFound()
} }
if goMsg == errInteractionNotAllowed {
return "", "", ErrInteractionNotAllowed
}
return "", "", errors.New(goMsg) return "", "", errors.New(goMsg)
} }
@@ -99,21 +110,30 @@ func (h Osxkeychain) Get(serverURL string) (string, string, error) {
// List returns the stored URLs and corresponding usernames. // List returns the stored URLs and corresponding usernames.
func (h Osxkeychain) List() (map[string]string, error) { func (h Osxkeychain) List() (map[string]string, error) {
credsLabelC := C.CString(credentials.CredsLabel)
defer C.free(unsafe.Pointer(credsLabelC))
var pathsC **C.char var pathsC **C.char
defer C.free(unsafe.Pointer(pathsC)) defer C.free(unsafe.Pointer(pathsC))
var acctsC **C.char var acctsC **C.char
defer C.free(unsafe.Pointer(acctsC)) defer C.free(unsafe.Pointer(acctsC))
var listLenC C.uint var listLenC C.uint
errMsg := C.keychain_list(&pathsC, &acctsC, &listLenC) errMsg := C.keychain_list(credsLabelC, &pathsC, &acctsC, &listLenC)
defer C.freeListData(&pathsC, listLenC)
defer C.freeListData(&acctsC, listLenC)
if errMsg != nil { if errMsg != nil {
defer C.free(unsafe.Pointer(errMsg)) defer C.free(unsafe.Pointer(errMsg))
goMsg := C.GoString(errMsg) goMsg := C.GoString(errMsg)
if goMsg == errCredentialsNotFound {
return make(map[string]string), nil
}
if goMsg == errInteractionNotAllowed {
return nil, ErrInteractionNotAllowed
}
return nil, errors.New(goMsg) return nil, errors.New(goMsg)
} }
defer C.freeListData(&pathsC, listLenC)
defer C.freeListData(&acctsC, listLenC)
var listLen int var listLen int
listLen = int(listLenC) listLen = int(listLenC)
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen] pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
@@ -130,30 +150,27 @@ func (h Osxkeychain) List() (map[string]string, error) {
} }
func splitServer(serverURL string) (*C.struct_Server, error) { func splitServer(serverURL string) (*C.struct_Server, error) {
u, err := url.Parse(serverURL) u, err := registryurl.Parse(serverURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hostAndPort := strings.Split(u.Host, ":") proto := C.kSecProtocolTypeHTTPS
host := hostAndPort[0] if u.Scheme == "http" {
proto = C.kSecProtocolTypeHTTP
}
var port int var port int
if len(hostAndPort) == 2 { p := registryurl.GetPort(u)
p, err := strconv.Atoi(hostAndPort[1]) if p != "" {
port, err = strconv.Atoi(p)
if err != nil { if err != nil {
return nil, err return nil, err
} }
port = p
}
proto := C.kSecProtocolTypeHTTPS
if u.Scheme != "https" {
proto = C.kSecProtocolTypeHTTP
} }
return &C.struct_Server{ return &C.struct_Server{
proto: C.SecProtocolType(proto), proto: C.SecProtocolType(proto),
host: C.CString(host), host: C.CString(registryurl.GetHostname(u)),
port: C.uint(port), port: C.uint(port),
path: C.CString(u.Path), path: C.CString(u.Path),
}, nil }, nil
+2 -2
View File
@@ -7,8 +7,8 @@ struct Server {
unsigned int port; unsigned int port;
}; };
char *keychain_add(struct Server *server, char *username, char *secret); char *keychain_add(struct Server *server, char *label, char *username, char *secret);
char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *secret_l, char **secret); char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *secret_l, char **secret);
char *keychain_delete(struct Server *server); char *keychain_delete(struct Server *server);
char *keychain_list(char *** data, char *** accts, unsigned int *list_l); char *keychain_list(char *credsLabel, char *** data, char *** accts, unsigned int *list_l);
void freeListData(char *** data, unsigned int length); void freeListData(char *** data, unsigned int length);
+151 -1
View File
@@ -1,8 +1,10 @@
package osxkeychain package osxkeychain
import ( import (
"github.com/docker/docker-credential-helpers/credentials" "fmt"
"testing" "testing"
"github.com/docker/docker-credential-helpers/credentials"
) )
func TestOSXKeychainHelper(t *testing.T) { func TestOSXKeychainHelper(t *testing.T) {
@@ -54,6 +56,154 @@ func TestOSXKeychainHelper(t *testing.T) {
} }
} }
// TestOSXKeychainHelperRetrieveAliases verifies that secrets can be accessed
// through variations on the URL
func TestOSXKeychainHelperRetrieveAliases(t *testing.T) {
tests := []struct {
storeURL string
readURL string
}{
// stored with port, retrieved without
{"https://foobar.docker.io:2376", "https://foobar.docker.io"},
// stored as https, retrieved without scheme
{"https://foobar.docker.io:2376", "foobar.docker.io"},
// stored with path, retrieved without
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234"},
}
helper := Osxkeychain{}
defer func() {
for _, te := range tests {
helper.Delete(te.storeURL)
}
}()
// Clean store before testing.
for _, te := range tests {
helper.Delete(te.storeURL)
}
for _, te := range tests {
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
continue
}
if _, _, err := helper.Get(te.readURL); err != nil {
t.Errorf("Error: failed to read secret for URL %q using %q", te.storeURL, te.readURL)
}
helper.Delete(te.storeURL)
}
}
// TestOSXKeychainHelperRetrieveStrict verifies that only matching secrets are
// returned.
func TestOSXKeychainHelperRetrieveStrict(t *testing.T) {
tests := []struct {
storeURL string
readURL string
}{
// stored as https, retrieved using http
{"https://foobar.docker.io:2376", "http://foobar.docker.io:2376"},
// stored as http, retrieved using https
{"http://foobar.docker.io:2376", "https://foobar.docker.io:2376"},
// same: stored as http, retrieved without a scheme specified (hence, using the default https://)
{"http://foobar.docker.io", "foobar.docker.io:5678"},
// non-matching ports
{"https://foobar.docker.io:1234", "https://foobar.docker.io:5678"},
// non-matching ports TODO is this desired behavior? The other way round does work
//{"https://foobar.docker.io", "https://foobar.docker.io:5678"},
// non-matching paths
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234/five/six"},
}
helper := Osxkeychain{}
defer func() {
for _, te := range tests {
helper.Delete(te.storeURL)
}
}()
// Clean store before testing.
for _, te := range tests {
helper.Delete(te.storeURL)
}
for _, te := range tests {
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
continue
}
if _, _, err := helper.Get(te.readURL); err == nil {
t.Errorf("Error: managed to read secret for URL %q using %q, but should not be able to", te.storeURL, te.readURL)
}
helper.Delete(te.storeURL)
}
}
// TestOSXKeychainHelperStoreRetrieve verifies that secrets stored in the
// the keychain can be read back using the URL that was used to store them.
func TestOSXKeychainHelperStoreRetrieve(t *testing.T) {
tests := []struct {
url string
}{
{url: "foobar.docker.io"},
{url: "foobar.docker.io:2376"},
{url: "//foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376"},
{url: "http://foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376/some/path"},
{url: "https://foobar.docker.io:2376/some/other/path"},
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar"},
}
helper := Osxkeychain{}
defer func() {
for _, te := range tests {
helper.Delete(te.url)
}
}()
// Clean store before testing.
for _, te := range tests {
helper.Delete(te.url)
}
// Note that we don't delete between individual tests here, to verify that
// subsequent stores/overwrites don't affect storing / retrieving secrets.
for i, te := range tests {
c := &credentials.Credentials{
ServerURL: te.url,
Username: fmt.Sprintf("user-%d", i),
Secret: fmt.Sprintf("secret-%d", i),
}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL: %s: %s", te.url, err)
continue
}
user, secret, err := helper.Get(te.url)
if err != nil {
t.Errorf("Error: failed to read secret for URL %q: %s", te.url, err)
continue
}
if user != c.Username {
t.Errorf("Error: expected username %s, got username %s for URL: %s", c.Username, user, te.url)
}
if secret != c.Secret {
t.Errorf("Error: expected secret %s, got secret %s for URL: %s", c.Secret, secret, te.url)
}
}
}
func TestMissingCredentials(t *testing.T) { func TestMissingCredentials(t *testing.T) {
helper := Osxkeychain{} helper := Osxkeychain{}
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd") _, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
+10
View File
@@ -0,0 +1,10 @@
package main
import (
"github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/pass"
)
func main() {
credentials.Serve(pass.Pass{})
}
+193
View File
@@ -0,0 +1,193 @@
// Package pass implements a `pass` based credential helper. Passwords are stored
// as arguments to pass of the form: "$PASS_FOLDER/base64-url(serverURL)/username".
// We base64-url encode the serverURL, because under the hood pass uses files and
// folders, so /s will get translated into additional folders.
package pass
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"sync"
"github.com/docker/docker-credential-helpers/credentials"
)
// PASS_FOLDER contains the directory where credentials are stored
const PASS_FOLDER = "docker-credential-helpers" //nolint: golint
// Pass handles secrets using Linux secret-service as a store.
type Pass struct{}
// Ideally these would be stored as members of Pass, but since all of Pass's
// methods have value receivers, not pointer receivers, and changing that is
// backwards incompatible, we assume that all Pass instances share the same configuration
// initializationMutex is held while initializing so that only one 'pass'
// round-tripping is done to check pass is functioning.
var initializationMutex sync.Mutex
var passInitialized bool
// CheckInitialized checks whether the password helper can be used. It
// internally caches and so may be safely called multiple times with no impact
// on performance, though the first call may take longer.
func (p Pass) CheckInitialized() bool {
return p.checkInitialized() == nil
}
func (p Pass) checkInitialized() error {
initializationMutex.Lock()
defer initializationMutex.Unlock()
if passInitialized {
return nil
}
// We just run a `pass ls`, if it fails then pass is not initialized.
_, err := p.runPassHelper("", "ls")
if err != nil {
return fmt.Errorf("pass not initialized: %v", err)
}
passInitialized = true
return nil
}
func (p Pass) runPass(stdinContent string, args ...string) (string, error) {
if err := p.checkInitialized(); err != nil {
return "", err
}
return p.runPassHelper(stdinContent, args...)
}
func (p Pass) runPassHelper(stdinContent string, args ...string) (string, error) {
var stdout, stderr bytes.Buffer
cmd := exec.Command("pass", args...)
cmd.Stdin = strings.NewReader(stdinContent)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return "", fmt.Errorf("%s: %s", err, stderr.String())
}
// trim newlines; pass v1.7.1+ includes a newline at the end of `show` output
return strings.TrimRight(stdout.String(), "\n\r"), nil
}
// Add adds new credentials to the keychain.
func (p Pass) Add(creds *credentials.Credentials) error {
if creds == nil {
return errors.New("missing credentials")
}
encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL))
_, err := p.runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
return err
}
// Delete removes credentials from the store.
func (p Pass) Delete(serverURL string) error {
if serverURL == "" {
return errors.New("missing server url")
}
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
_, err := p.runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
return err
}
func getPassDir() string {
passDir := "$HOME/.password-store"
if envDir := os.Getenv("PASSWORD_STORE_DIR"); envDir != "" {
passDir = envDir
}
return os.ExpandEnv(passDir)
}
// listPassDir lists all the contents of a directory in the password store.
// Pass uses fancy unicode to emit stuff to stdout, so rather than try
// and parse this, let's just look at the directory structure instead.
func listPassDir(args ...string) ([]os.FileInfo, error) {
passDir := getPassDir()
p := path.Join(append([]string{passDir, PASS_FOLDER}, args...)...)
contents, err := ioutil.ReadDir(p)
if err != nil {
if os.IsNotExist(err) {
return []os.FileInfo{}, nil
}
return nil, err
}
return contents, nil
}
// Get returns the username and secret to use for a given registry server URL.
func (p Pass) Get(serverURL string) (string, string, error) {
if serverURL == "" {
return "", "", errors.New("missing server url")
}
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
if _, err := os.Stat(path.Join(getPassDir(), PASS_FOLDER, encoded)); err != nil {
if os.IsNotExist(err) {
return "", "", nil
}
return "", "", err
}
usernames, err := listPassDir(encoded)
if err != nil {
return "", "", err
}
if len(usernames) < 1 {
return "", "", fmt.Errorf("no usernames for %s", serverURL)
}
actual := strings.TrimSuffix(usernames[0].Name(), ".gpg")
secret, err := p.runPass("", "show", path.Join(PASS_FOLDER, encoded, actual))
return actual, secret, err
}
// List returns the stored URLs and corresponding usernames for a given credentials label
func (p Pass) List() (map[string]string, error) {
servers, err := listPassDir()
if err != nil {
return nil, err
}
resp := map[string]string{}
for _, server := range servers {
if !server.IsDir() {
continue
}
serverURL, err := base64.URLEncoding.DecodeString(server.Name())
if err != nil {
return nil, err
}
usernames, err := listPassDir(server.Name())
if err != nil {
return nil, err
}
if len(usernames) < 1 {
return nil, fmt.Errorf("no usernames for %s", serverURL)
}
resp[string(serverURL)] = strings.TrimSuffix(usernames[0].Name(), ".gpg")
}
return resp, nil
}
+75
View File
@@ -0,0 +1,75 @@
package pass
import (
"strings"
"testing"
"github.com/docker/docker-credential-helpers/credentials"
)
func TestPassHelper(t *testing.T) {
helper := Pass{}
creds := &credentials.Credentials{
ServerURL: "https://foobar.docker.io:2376/v1",
Username: "nothing",
Secret: "isthebestmeshuggahalbum",
}
helper.Add(creds)
creds.ServerURL = "https://foobar.docker.io:9999/v2"
helper.Add(creds)
credsList, err := helper.List()
if err != nil {
t.Fatal(err)
}
for server, username := range credsList {
if !(strings.Contains(server, "2376") ||
strings.Contains(server, "9999")) {
t.Fatalf("invalid url: %s", creds.ServerURL)
}
if username != "nothing" {
t.Fatalf("invalid username: %v", username)
}
u, s, err := helper.Get(server)
if err != nil {
t.Fatal(err)
}
if u != username {
t.Fatalf("invalid username %s", u)
}
if s != "isthebestmeshuggahalbum" {
t.Fatalf("invalid secret: %s", s)
}
err = helper.Delete(server)
if err != nil {
t.Fatal(err)
}
username, _, err = helper.Get(server)
if err != nil {
t.Fatal(err)
}
if username != "" {
t.Fatalf("%s shouldn't exist any more", username)
}
}
credsList, err = helper.List()
if err != nil {
t.Fatal(err)
}
if len(credsList) != 0 {
t.Fatal("didn't delete all creds?")
}
}
+37
View File
@@ -0,0 +1,37 @@
package registryurl
import (
"errors"
"net/url"
"strings"
)
// Parse parses and validates a given serverURL to an url.URL, and
// returns an error if validation failed. Querystring parameters are
// omitted in the resulting URL, because they are not used in the helper.
//
// If serverURL does not have a valid scheme, `//` is used as scheme
// before parsing. This prevents the hostname being used as path,
// and the credentials being stored without host.
func Parse(registryURL string) (*url.URL, error) {
// Check if registryURL has a scheme, otherwise add `//` as scheme.
if !strings.Contains(registryURL, "://") && !strings.HasPrefix(registryURL, "//") {
registryURL = "//" + registryURL
}
u, err := url.Parse(registryURL)
if err != nil {
return nil, err
}
if u.Scheme != "" && u.Scheme != "https" && u.Scheme != "http" {
return nil, errors.New("unsupported scheme: " + u.Scheme)
}
if GetHostname(u) == "" {
return nil, errors.New("no hostname in URL")
}
u.RawQuery = ""
return u, nil
}
+46
View File
@@ -0,0 +1,46 @@
package registryurl
import (
"errors"
"testing"
)
// TestHelperParseURL verifies that a // "scheme" is added to URLs,
// and that invalid URLs produce an error.
func TestHelperParseURL(t *testing.T) {
tests := []struct {
url string
expectedURL string
err error
}{
{url: "foobar.docker.io", expectedURL: "//foobar.docker.io"},
{url: "foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
{url: "//foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
{url: "http://foobar.docker.io:2376", expectedURL: "http://foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376", expectedURL: "https://foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376/some/path", expectedURL: "https://foobar.docker.io:2376/some/path"},
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar", expectedURL: "https://foobar.docker.io:2376/some/other/path"},
{url: "/foobar.docker.io", err: errors.New("no hostname in URL")},
{url: "ftp://foobar.docker.io:2376", err: errors.New("unsupported scheme: ftp")},
}
for _, te := range tests {
u, err := Parse(te.url)
if te.err == nil && err != nil {
t.Errorf("Error: failed to parse URL %q: %s", te.url, err)
continue
}
if te.err != nil && err == nil {
t.Errorf("Error: expected error %q, got none when parsing URL %q", te.err, te.url)
continue
}
if te.err != nil && err.Error() != te.err.Error() {
t.Errorf("Error: expected error %q, got %q when parsing URL %q", te.err, err, te.url)
continue
}
if u != nil && u.String() != te.expectedURL {
t.Errorf("Error: expected URL: %q, but got %q for URL: %q", te.expectedURL, u.String(), te.url)
}
}
}
+17
View File
@@ -0,0 +1,17 @@
//+build go1.8
package registryurl
import (
url "net/url"
)
// GetHostname returns the hostname of the URL
func GetHostname(u *url.URL) string {
return u.Hostname()
}
// GetPort returns the port number of the URL
func GetPort(u *url.URL) string {
return u.Port()
}
+41
View File
@@ -0,0 +1,41 @@
//+build !go1.8
package registryurl
import (
url "net/url"
"strings"
)
func GetHostname(u *url.URL) string {
return stripPort(u.Host)
}
func GetPort(u *url.URL) string {
return portOnly(u.Host)
}
func stripPort(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
return hostport
}
if i := strings.IndexByte(hostport, ']'); i != -1 {
return strings.TrimPrefix(hostport[:i], "[")
}
return hostport[:colon]
}
func portOnly(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
return ""
}
if i := strings.Index(hostport, "]:"); i != -1 {
return hostport[i+len("]:"):]
}
if strings.Contains(hostport, "]") {
return ""
}
return hostport[colon+len(":"):]
}
+35 -22
View File
@@ -7,6 +7,7 @@ const SecretSchema *docker_get_schema(void)
static const SecretSchema docker_schema = { static const SecretSchema docker_schema = {
"io.docker.Credentials", SECRET_SCHEMA_NONE, "io.docker.Credentials", SECRET_SCHEMA_NONE,
{ {
{ "label", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "server", SECRET_SCHEMA_ATTRIBUTE_STRING }, { "server", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "username", SECRET_SCHEMA_ATTRIBUTE_STRING }, { "username", SECRET_SCHEMA_ATTRIBUTE_STRING },
{ "docker_cli", SECRET_SCHEMA_ATTRIBUTE_STRING }, { "docker_cli", SECRET_SCHEMA_ATTRIBUTE_STRING },
@@ -16,11 +17,12 @@ const SecretSchema *docker_get_schema(void)
return &docker_schema; return &docker_schema;
} }
GError *add(char *server, char *username, char *secret) { GError *add(char *label, char *server, char *username, char *secret) {
GError *err = NULL; GError *err = NULL;
secret_password_store_sync (DOCKER_SCHEMA, SECRET_COLLECTION_DEFAULT, secret_password_store_sync (DOCKER_SCHEMA, SECRET_COLLECTION_DEFAULT,
server, secret, NULL, &err, server, secret, NULL, &err,
"label", label,
"server", server, "server", server,
"username", username, "username", username,
"docker_cli", "1", "docker_cli", "1",
@@ -40,7 +42,7 @@ GError *delete(char *server) {
return NULL; return NULL;
} }
char *get_username(SecretItem *item) { char *get_attribute(const char *attribute, SecretItem *item) {
GHashTable *attributes; GHashTable *attributes;
GHashTableIter iter; GHashTableIter iter;
gchar *value, *key; gchar *value, *key;
@@ -48,7 +50,7 @@ char *get_username(SecretItem *item) {
attributes = secret_item_get_attributes(item); attributes = secret_item_get_attributes(item);
g_hash_table_iter_init(&iter, attributes); g_hash_table_iter_init(&iter, attributes);
while (g_hash_table_iter_next(&iter, (void **)&key, (void **)&value)) { while (g_hash_table_iter_next(&iter, (void **)&key, (void **)&value)) {
if (strncmp(key, "username", strlen(key)) == 0) if (strncmp(key, attribute, strlen(key)) == 0)
return (char *)value; return (char *)value;
} }
g_hash_table_unref(attributes); g_hash_table_unref(attributes);
@@ -71,7 +73,7 @@ GError *get(char *server, char **username, char **secret) {
service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err); service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err);
if (err == NULL) { if (err == NULL) {
items = secret_service_search_sync(service, NULL, attributes, flags, NULL, &err); items = secret_service_search_sync(service, DOCKER_SCHEMA, attributes, flags, NULL, &err);
if (err == NULL) { if (err == NULL) {
for (l = items; l != NULL; l = g_list_next(l)) { for (l = items; l != NULL; l = g_list_next(l)) {
value = secret_item_get_schema_name(l->data); value = secret_item_get_schema_name(l->data);
@@ -85,7 +87,7 @@ GError *get(char *server, char **username, char **secret) {
*secret = strdup(secret_value_get(secretValue, &length)); *secret = strdup(secret_value_get(secretValue, &length));
secret_value_unref(secretValue); secret_value_unref(secretValue);
} }
*username = get_username(l->data); *username = get_attribute("username", l->data);
} }
g_list_free_full(items, g_object_unref); g_list_free_full(items, g_object_unref);
} }
@@ -98,22 +100,30 @@ GError *get(char *server, char **username, char **secret) {
return NULL; return NULL;
} }
GError *list(char *** paths, char *** accts, unsigned int *list_l) { GError *list(char *ref_label, char *** paths, char *** accts, unsigned int *list_l) {
GList *items; GList *items;
GError *err = NULL; GError *err = NULL;
SecretService *service; SecretService *service;
SecretSearchFlags flags = SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK; SecretSearchFlags flags = SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK;
GHashTable *attributes; GHashTable *attributes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
attributes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); // List credentials with the right label only
g_hash_table_insert(attributes, g_strdup("label"), g_strdup(ref_label));
service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err); service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err);
if (err != NULL) {
return err;
}
items = secret_service_search_sync(service, NULL, attributes, flags, NULL, &err); items = secret_service_search_sync(service, NULL, attributes, flags, NULL, &err);
int numKeys = g_list_length(items); int numKeys = g_list_length(items);
if (err != NULL) { if (err != NULL) {
return err; return err;
} }
*paths = (char **) malloc((int)sizeof(char *)*numKeys);
*accts = (char **) malloc((int)sizeof(char *)*numKeys); char **tmp_paths = (char **) calloc(1,(int)sizeof(char *)*numKeys);
char **tmp_accts = (char **) calloc(1,(int)sizeof(char *)*numKeys);
// items now contains our keys from the gnome keyring // items now contains our keys from the gnome keyring
// we will now put it in our two lists to return it to go // we will now put it in our two lists to return it to go
GList *current; GList *current;
@@ -121,21 +131,25 @@ GError *list(char *** paths, char *** accts, unsigned int *list_l) {
for(current = items; current!=NULL; current = current->next) { for(current = items; current!=NULL; current = current->next) {
char *pathTmp = secret_item_get_label(current->data); char *pathTmp = secret_item_get_label(current->data);
// you cannot have a key without a label in the gnome keyring // you cannot have a key without a label in the gnome keyring
char *acctTmp = get_username(current->data); char *acctTmp = get_attribute("username",current->data);
if (acctTmp==NULL) { if (acctTmp==NULL) {
acctTmp = "account not defined"; acctTmp = "account not defined";
} }
char *path = (char *) malloc(strlen(pathTmp));
char *acct = (char *) malloc(strlen(acctTmp)); tmp_paths[listNumber] = (char *) calloc(1, sizeof(char)*(strlen(pathTmp)+1));
path = pathTmp; tmp_accts[listNumber] = (char *) calloc(1, sizeof(char)*(strlen(acctTmp)+1));
acct = acctTmp;
(*paths)[listNumber] = (char *) malloc(sizeof(char)*(strlen(path))); memcpy(tmp_paths[listNumber], pathTmp, sizeof(char)*(strlen(pathTmp)+1));
memcpy((*paths)[listNumber], path, sizeof(char)*(strlen(path))); memcpy(tmp_accts[listNumber], acctTmp, sizeof(char)*(strlen(acctTmp)+1));
(*accts)[listNumber] = (char *) malloc(sizeof(char)*(strlen(acct)));
memcpy((*accts)[listNumber], acct, sizeof(char)*(strlen(acct)));
listNumber = listNumber + 1; listNumber = listNumber + 1;
} }
*list_l = numKeys;
*paths = (char **) realloc(tmp_paths, (int)sizeof(char *)*listNumber);
*accts = (char **) realloc(tmp_accts, (int)sizeof(char *)*listNumber);
*list_l = listNumber;
return NULL; return NULL;
} }
@@ -144,5 +158,4 @@ void freeListData(char *** data, unsigned int length) {
for(i=0; i<length; i++) { for(i=0; i<length; i++) {
free((*data)[i]); free((*data)[i]);
} }
free(*data);
} }
+22 -10
View File
@@ -22,6 +22,8 @@ func (h Secretservice) Add(creds *credentials.Credentials) error {
if creds == nil { if creds == nil {
return errors.New("missing credentials") return errors.New("missing credentials")
} }
credsLabel := C.CString(credentials.CredsLabel)
defer C.free(unsafe.Pointer(credsLabel))
server := C.CString(creds.ServerURL) server := C.CString(creds.ServerURL)
defer C.free(unsafe.Pointer(server)) defer C.free(unsafe.Pointer(server))
username := C.CString(creds.Username) username := C.CString(creds.Username)
@@ -29,7 +31,7 @@ func (h Secretservice) Add(creds *credentials.Credentials) error {
secret := C.CString(creds.Secret) secret := C.CString(creds.Secret)
defer C.free(unsafe.Pointer(secret)) defer C.free(unsafe.Pointer(secret))
if err := C.add(server, username, secret); err != nil { if err := C.add(credsLabel, server, username, secret); err != nil {
defer C.g_error_free(err) defer C.g_error_free(err)
errMsg := (*C.char)(unsafe.Pointer(err.message)) errMsg := (*C.char)(unsafe.Pointer(err.message))
return errors.New(C.GoString(errMsg)) return errors.New(C.GoString(errMsg))
@@ -79,25 +81,35 @@ func (h Secretservice) Get(serverURL string) (string, string, error) {
return user, pass, nil return user, pass, nil
} }
// List returns the stored URLs and corresponding usernames. // List returns the stored URLs and corresponding usernames for a given credentials label
func (h Secretservice) List() (map[string]string, error) { func (h Secretservice) List() (map[string]string, error) {
credsLabelC := C.CString(credentials.CredsLabel)
defer C.free(unsafe.Pointer(credsLabelC))
var pathsC **C.char var pathsC **C.char
defer C.free(unsafe.Pointer(pathsC)) defer C.free(unsafe.Pointer(pathsC))
var acctsC **C.char var acctsC **C.char
defer C.free(unsafe.Pointer(acctsC)) defer C.free(unsafe.Pointer(acctsC))
var listLenC C.uint var listLenC C.uint
err := C.list(&pathsC, &acctsC, &listLenC) err := C.list(credsLabelC, &pathsC, &acctsC, &listLenC)
if err != nil {
defer C.free(unsafe.Pointer(err))
return nil, errors.New("Error from list function in secretservice_linux.c likely due to error in secretservice library")
}
defer C.freeListData(&pathsC, listLenC) defer C.freeListData(&pathsC, listLenC)
defer C.freeListData(&acctsC, listLenC) defer C.freeListData(&acctsC, listLenC)
if err != nil {
defer C.g_error_free(err)
return nil, errors.New("Error from list function in secretservice_linux.c likely due to error in secretservice library")
}
resp := make(map[string]string)
listLen := int(listLenC) listLen := int(listLenC)
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen] if listLen == 0 {
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen] return resp, nil
resp := make(map[string]string) }
// The maximum capacity of the following two slices is limited to (2^29)-1 to remain compatible
// with 32-bit platforms. The size of a `*C.char` (a pointer) is 4 Byte on a 32-bit system
// and (2^29)*4 == math.MaxInt32 + 1. -- See issue golang/go#13656
pathTmp := (*[(1 << 29) - 1]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
acctTmp := (*[(1 << 29) - 1]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
for i := 0; i < listLen; i++ { for i := 0; i < listLen; i++ {
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i]) resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i])
} }
+2 -2
View File
@@ -6,8 +6,8 @@ const SecretSchema *docker_get_schema(void) G_GNUC_CONST;
#define DOCKER_SCHEMA docker_get_schema() #define DOCKER_SCHEMA docker_get_schema()
GError *add(char *server, char *username, char *secret); GError *add(char *label, char *server, char *username, char *secret);
GError *delete(char *server); GError *delete(char *server);
GError *get(char *server, char **username, char **secret); GError *get(char *server, char **username, char **secret);
GError *list(char *** paths, char *** accts, unsigned int *list_l); GError *list(char *label, char *** paths, char *** accts, unsigned int *list_l);
void freeListData(char *** data, unsigned int length); void freeListData(char *** data, unsigned int length);
+39 -6
View File
@@ -1,6 +1,7 @@
package secretservice package secretservice
import ( import (
"strings"
"testing" "testing"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
@@ -16,10 +17,36 @@ func TestSecretServiceHelper(t *testing.T) {
} }
helper := Secretservice{} helper := Secretservice{}
// Check how many docker credentials we have when starting the test
oldAuths, err := helper.List()
if err != nil {
t.Fatal(err)
}
// If any docker credentials with the tests values we are providing, we
// remove them as they probably come from a previous failed test
for k, v := range oldAuths {
if strings.Compare(k, creds.ServerURL) == 0 && strings.Compare(v, creds.Username) == 0 {
if err := helper.Delete(creds.ServerURL); err != nil {
t.Fatal(err)
}
}
}
// Check again how many docker credentials we have when starting the test
oldAuths, err = helper.List()
if err != nil {
t.Fatal(err)
}
// Add new credentials
if err := helper.Add(creds); err != nil { if err := helper.Add(creds); err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Verify that it is inside the secret service store
username, secret, err := helper.Get(creds.ServerURL) username, secret, err := helper.Get(creds.ServerURL)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -33,15 +60,21 @@ func TestSecretServiceHelper(t *testing.T) {
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret) t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
} }
// We should have one more credential than before adding
newAuths, err := helper.List()
if err != nil || (len(newAuths)-len(oldAuths) != 1) {
t.Fatal(err)
}
oldAuths = newAuths
// Deleting the credentials associated to current server url should succeed
if err := helper.Delete(creds.ServerURL); err != nil { if err := helper.Delete(creds.ServerURL); err != nil {
t.Fatal(err) t.Fatal(err)
} }
auths, err := helper.List()
if err != nil || len(auths) == 0 { // We should have one less credential than before deleting
t.Fatal(err) newAuths, err = helper.List()
} if err != nil || (len(oldAuths)-len(newAuths) != 1) {
helper.Add(creds)
if newauths, err := helper.List(); (len(newauths) - len(auths)) != 1 {
t.Fatal(err) t.Fatal(err)
} }
} }
+1
View File
@@ -0,0 +1 @@
*.go text eol=lf
+23
View File
@@ -0,0 +1,23 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
+98
View File
@@ -0,0 +1,98 @@
wincred
=======
Go wrapper around the Windows Credential Manager API functions.
![Go](https://github.com/danieljoos/wincred/workflows/Go/badge.svg)
[![GoDoc](https://godoc.org/github.com/danieljoos/wincred?status.svg)](https://godoc.org/github.com/danieljoos/wincred)
Installation
------------
```Go
go get github.com/danieljoos/wincred
```
Usage
-----
See the following examples:
### Create and store a new generic credential object
```Go
package main
import (
"fmt"
"github.com/danieljoos/wincred"
)
func main() {
cred := wincred.NewGenericCredential("myGoApplication")
cred.CredentialBlob = []byte("my secret")
err := cred.Write()
if err != nil {
fmt.Println(err)
}
}
```
### Retrieve a credential object
```Go
package main
import (
"fmt"
"github.com/danieljoos/wincred"
)
func main() {
cred, err := wincred.GetGenericCredential("myGoApplication")
if err == nil {
fmt.Println(string(cred.CredentialBlob))
}
}
```
### Remove a credential object
```Go
package main
import (
"fmt"
"github.com/danieljoos/wincred"
)
func main() {
cred, err := wincred.GetGenericCredential("myGoApplication")
if err != nil {
fmt.Println(err)
return
}
cred.Delete()
}
```
### List all available credentials
```Go
package main
import (
"fmt"
"github.com/danieljoos/wincred"
)
func main() {
creds, err := wincred.List()
if err != nil {
fmt.Println(err)
return
}
for i := range(creds) {
fmt.Println(creds[i].TargetName)
}
}
```
+40 -30
View File
@@ -1,3 +1,5 @@
// +build windows
package wincred package wincred
import ( import (
@@ -9,48 +11,57 @@ import (
"unsafe" "unsafe"
) )
var nullPointer = unsafe.Pointer(uintptr(0)) // uf16PtrToString creates a Go string from a pointer to a UTF16 encoded zero-terminated string.
// Such pointers are returned from the Windows API calls.
// Create a Go string using a pointer to a zero-terminated UTF 16 encoded string. // The function creates a copy of the string.
// See github.com/AllenDang/w32
func utf16PtrToString(wstr *uint16) string { func utf16PtrToString(wstr *uint16) string {
if wstr != nil { if wstr != nil {
buf := make([]uint16, 0, 256) for len := 0; ; len++ {
for ptr := uintptr(unsafe.Pointer(wstr)); ; ptr += 2 { ptr := unsafe.Pointer(uintptr(unsafe.Pointer(wstr)) + uintptr(len)*unsafe.Sizeof(*wstr)) // see https://golang.org/pkg/unsafe/#Pointer (3)
rune := *(*uint16)(unsafe.Pointer(ptr)) if *(*uint16)(ptr) == 0 {
if rune == 0 { return string(utf16.Decode(*(*[]uint16)(unsafe.Pointer(&reflect.SliceHeader{
return string(utf16.Decode(buf)) Data: uintptr(unsafe.Pointer(wstr)),
Len: len,
Cap: len,
}))))
} }
buf = append(buf, rune)
} }
} }
return "" return ""
} }
// Create a byte array from a given UTF 16 char array // utf16ToByte creates a byte array from a given UTF 16 char array.
func utf16ToByte(wstr []uint16) (result []byte) { func utf16ToByte(wstr []uint16) (result []byte) {
result = make([]byte, len(wstr)*2) result = make([]byte, len(wstr)*2)
for i, _ := range wstr { for i := range wstr {
binary.LittleEndian.PutUint16(result[(i*2):(i*2)+2], wstr[i]) binary.LittleEndian.PutUint16(result[(i*2):(i*2)+2], wstr[i])
} }
return return
} }
// Copies the given C byte array to a Go byte array (see `C.GoBytes`) // utf16FromString creates a UTF16 char array from a string.
func goBytes(src unsafe.Pointer, len uint32) []byte { func utf16FromString(str string) []uint16 {
if src == nullPointer { return syscall.StringToUTF16(str)
}
// 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{} return []byte{}
} }
slice := (*[1 << 30]byte)(src)[0:len]
rv := make([]byte, len) rv := make([]byte, len)
copy(rv, slice) copy(rv, *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
return rv[:] Data: src,
Len: int(len),
Cap: int(len),
})))
return rv
} }
// Convert the given CREDENTIAL struct to a more usable structure // Convert the given CREDENTIAL struct to a more usable structure
func nativeToCredential(cred *nativeCREDENTIAL) (result *Credential) { func sysToCredential(cred *sysCREDENTIAL) (result *Credential) {
if unsafe.Pointer(cred) == nullPointer { if cred == nil {
return nil return nil
} }
result = new(Credential) result = new(Credential)
@@ -60,29 +71,28 @@ func nativeToCredential(cred *nativeCREDENTIAL) (result *Credential) {
result.UserName = utf16PtrToString(cred.UserName) result.UserName = utf16PtrToString(cred.UserName)
result.LastWritten = time.Unix(0, cred.LastWritten.Nanoseconds()) result.LastWritten = time.Unix(0, cred.LastWritten.Nanoseconds())
result.Persist = CredentialPersistence(cred.Persist) result.Persist = CredentialPersistence(cred.Persist)
result.CredentialBlob = goBytes(unsafe.Pointer(cred.CredentialBlob), cred.CredentialBlobSize) result.CredentialBlob = goBytes(cred.CredentialBlob, cred.CredentialBlobSize)
result.Attributes = make([]CredentialAttribute, cred.AttributeCount) result.Attributes = make([]CredentialAttribute, cred.AttributeCount)
attrSliceHeader := reflect.SliceHeader{ attrSlice := *(*[]sysCREDENTIAL_ATTRIBUTE)(unsafe.Pointer(&reflect.SliceHeader{
Data: cred.Attributes, Data: cred.Attributes,
Len: int(cred.AttributeCount), Len: int(cred.AttributeCount),
Cap: int(cred.AttributeCount), Cap: int(cred.AttributeCount),
} }))
attrSlice := *(*[]nativeCREDENTIAL_ATTRIBUTE)(unsafe.Pointer(&attrSliceHeader))
for i, attr := range attrSlice { for i, attr := range attrSlice {
resultAttr := &result.Attributes[i] resultAttr := &result.Attributes[i]
resultAttr.Keyword = utf16PtrToString(attr.Keyword) resultAttr.Keyword = utf16PtrToString(attr.Keyword)
resultAttr.Value = goBytes(unsafe.Pointer(attr.Value), attr.ValueSize) resultAttr.Value = goBytes(attr.Value, attr.ValueSize)
} }
return result return result
} }
// Convert the given Credential object back to a CREDENTIAL struct, which can be used for calling the // Convert the given Credential object back to a CREDENTIAL struct, which can be used for calling the
// Windows APIs // Windows APIs
func nativeFromCredential(cred *Credential) (result *nativeCREDENTIAL) { func sysFromCredential(cred *Credential) (result *sysCREDENTIAL) {
if cred == nil { if cred == nil {
return nil return nil
} }
result = new(nativeCREDENTIAL) result = new(sysCREDENTIAL)
result.Flags = 0 result.Flags = 0
result.Type = 0 result.Type = 0
result.TargetName, _ = syscall.UTF16PtrFromString(cred.TargetName) result.TargetName, _ = syscall.UTF16PtrFromString(cred.TargetName)
@@ -96,13 +106,13 @@ func nativeFromCredential(cred *Credential) (result *nativeCREDENTIAL) {
} }
result.Persist = uint32(cred.Persist) result.Persist = uint32(cred.Persist)
result.AttributeCount = uint32(len(cred.Attributes)) result.AttributeCount = uint32(len(cred.Attributes))
attributes := make([]nativeCREDENTIAL_ATTRIBUTE, len(cred.Attributes)) attributes := make([]sysCREDENTIAL_ATTRIBUTE, len(cred.Attributes))
if len(attributes) > 0 { if len(attributes) > 0 {
result.Attributes = uintptr(unsafe.Pointer(&attributes[0])) result.Attributes = uintptr(unsafe.Pointer(&attributes[0]))
} else { } else {
result.Attributes = 0 result.Attributes = 0
} }
for i, _ := range cred.Attributes { for i := range cred.Attributes {
inAttr := &cred.Attributes[i] inAttr := &cred.Attributes[i]
outAttr := &attributes[i] outAttr := &attributes[i]
outAttr.Keyword, _ = syscall.UTF16PtrFromString(inAttr.Keyword) outAttr.Keyword, _ = syscall.UTF16PtrFromString(inAttr.Keyword)
+11
View File
@@ -0,0 +1,11 @@
// +build !windows
package wincred
func utf16ToByte(...interface{}) []byte {
return nil
}
func utf16FromString(...interface{}) []uint16 {
return nil
}
+5
View File
@@ -0,0 +1,5 @@
module github.com/danieljoos/wincred
go 1.13
require github.com/stretchr/testify v1.5.1
+12
View File
@@ -0,0 +1,12 @@
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/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/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-137
View File
@@ -1,137 +0,0 @@
package wincred
import (
"syscall"
"unsafe"
)
var (
modadvapi32 = syscall.NewLazyDLL("advapi32.dll")
procCredRead proc = modadvapi32.NewProc("CredReadW")
procCredWrite proc = modadvapi32.NewProc("CredWriteW")
procCredDelete proc = modadvapi32.NewProc("CredDeleteW")
procCredFree proc = modadvapi32.NewProc("CredFree")
procCredEnumerate proc = modadvapi32.NewProc("CredEnumerateW")
)
// Interface for syscall.Proc: helps testing
type proc interface {
Call(a ...uintptr) (r1, r2 uintptr, lastErr error)
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374788(v=vs.85).aspx
type nativeCREDENTIAL struct {
Flags uint32
Type uint32
TargetName *uint16
Comment *uint16
LastWritten syscall.Filetime
CredentialBlobSize uint32
CredentialBlob uintptr
Persist uint32
AttributeCount uint32
Attributes uintptr
TargetAlias *uint16
UserName *uint16
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374790(v=vs.85).aspx
type nativeCREDENTIAL_ATTRIBUTE struct {
Keyword *uint16
Flags uint32
ValueSize uint32
Value uintptr
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374788(v=vs.85).aspx
type nativeCRED_TYPE uint32
const (
naCRED_TYPE_GENERIC nativeCRED_TYPE = 0x1
naCRED_TYPE_DOMAIN_PASSWORD nativeCRED_TYPE = 0x2
naCRED_TYPE_DOMAIN_CERTIFICATE nativeCRED_TYPE = 0x3
naCRED_TYPE_DOMAIN_VISIBLE_PASSWORD nativeCRED_TYPE = 0x4
naCRED_TYPE_GENERIC_CERTIFICATE nativeCRED_TYPE = 0x5
naCRED_TYPE_DOMAIN_EXTENDED nativeCRED_TYPE = 0x6
naERROR_NOT_FOUND = "Element not found."
)
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374804(v=vs.85).aspx
func nativeCredRead(targetName string, typ nativeCRED_TYPE) (*Credential, error) {
var pcred uintptr
targetNamePtr, _ := syscall.UTF16PtrFromString(targetName)
ret, _, err := procCredRead.Call(
uintptr(unsafe.Pointer(targetNamePtr)),
uintptr(typ),
0,
uintptr(unsafe.Pointer(&pcred)),
)
if ret == 0 {
return nil, err
}
defer procCredFree.Call(pcred)
return nativeToCredential((*nativeCREDENTIAL)(unsafe.Pointer(pcred))), nil
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa375187(v=vs.85).aspx
func nativeCredWrite(cred *Credential, typ nativeCRED_TYPE) error {
ncred := nativeFromCredential(cred)
ncred.Type = uint32(typ)
ret, _, err := procCredWrite.Call(
uintptr(unsafe.Pointer(ncred)),
0,
)
if ret == 0 {
return err
}
return nil
}
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374787(v=vs.85).aspx
func nativeCredDelete(cred *Credential, typ nativeCRED_TYPE) error {
targetNamePtr, _ := syscall.UTF16PtrFromString(cred.TargetName)
ret, _, err := procCredDelete.Call(
uintptr(unsafe.Pointer(targetNamePtr)),
uintptr(typ),
0,
)
if ret == 0 {
return err
}
return nil
}
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374794(v=vs.85).aspx
func nativeCredEnumerate(filter string, all bool) ([]*Credential, error) {
var count int
var pcreds uintptr
var filterPtr uintptr
if !all {
filterUtf16Ptr, _ := syscall.UTF16PtrFromString(filter)
filterPtr = uintptr(unsafe.Pointer(filterUtf16Ptr))
} else {
filterPtr = 0
}
ret, _, err := procCredEnumerate.Call(
filterPtr,
0,
uintptr(unsafe.Pointer(&count)),
uintptr(unsafe.Pointer(&pcreds)),
)
if ret == 0 {
return nil, err
}
defer procCredFree.Call(pcreds)
pcredsSlice := (*[1 << 30]uintptr)(unsafe.Pointer(pcreds))[:count:count]
creds := make([]*Credential, count)
for i := range creds {
creds[i] = nativeToCredential((*nativeCREDENTIAL)(unsafe.Pointer(pcredsSlice[i])))
}
return creds, nil
}
+143
View File
@@ -0,0 +1,143 @@
// +build windows
package wincred
import (
"reflect"
"syscall"
"unsafe"
)
var (
modadvapi32 = syscall.NewLazyDLL("advapi32.dll")
procCredRead proc = modadvapi32.NewProc("CredReadW")
procCredWrite proc = modadvapi32.NewProc("CredWriteW")
procCredDelete proc = modadvapi32.NewProc("CredDeleteW")
procCredFree proc = modadvapi32.NewProc("CredFree")
procCredEnumerate proc = modadvapi32.NewProc("CredEnumerateW")
)
// Interface for syscall.Proc: helps testing
type proc interface {
Call(a ...uintptr) (r1, r2 uintptr, lastErr error)
}
// 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 syscall.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-_credential_attributew
type sysCREDENTIAL_ATTRIBUTE struct {
Keyword *uint16
Flags uint32
ValueSize uint32
Value uintptr
}
// 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/Debug/system-error-codes
sysERROR_NOT_FOUND = syscall.Errno(1168)
sysERROR_INVALID_PARAMETER = syscall.Errno(87)
)
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credreadw
func sysCredRead(targetName string, typ sysCRED_TYPE) (*Credential, error) {
var pcred *sysCREDENTIAL
targetNamePtr, _ := syscall.UTF16PtrFromString(targetName)
ret, _, err := procCredRead.Call(
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 sysToCredential(pcred), nil
}
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credwritew
func sysCredWrite(cred *Credential, typ sysCRED_TYPE) error {
ncred := sysFromCredential(cred)
ncred.Type = uint32(typ)
ret, _, err := procCredWrite.Call(
uintptr(unsafe.Pointer(ncred)),
0,
)
if ret == 0 {
return err
}
return nil
}
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-creddeletew
func sysCredDelete(cred *Credential, typ sysCRED_TYPE) error {
targetNamePtr, _ := syscall.UTF16PtrFromString(cred.TargetName)
ret, _, err := procCredDelete.Call(
uintptr(unsafe.Pointer(targetNamePtr)),
uintptr(typ),
0,
)
if ret == 0 {
return err
}
return nil
}
// https://docs.microsoft.com/en-us/windows/desktop/api/wincred/nf-wincred-credenumeratew
func sysCredEnumerate(filter string, all bool) ([]*Credential, error) {
var count int
var pcreds uintptr
var filterPtr *uint16
if !all {
filterPtr, _ = syscall.UTF16PtrFromString(filter)
}
ret, _, err := procCredEnumerate.Call(
uintptr(unsafe.Pointer(filterPtr)),
0,
uintptr(unsafe.Pointer(&count)),
uintptr(unsafe.Pointer(&pcreds)),
)
if ret == 0 {
return nil, err
}
defer procCredFree.Call(pcreds)
credsSlice := *(*[]*sysCREDENTIAL)(unsafe.Pointer(&reflect.SliceHeader{
Data: pcreds,
Len: count,
Cap: count,
}))
creds := make([]*Credential, count, count)
for i, cred := range credsSlice {
creds[i] = sysToCredential(cred)
}
return creds, nil
}
+36
View File
@@ -0,0 +1,36 @@
// +build !windows
package wincred
import (
"errors"
"syscall"
)
const (
sysCRED_TYPE_GENERIC = 0
sysCRED_TYPE_DOMAIN_PASSWORD = 0
sysCRED_TYPE_DOMAIN_CERTIFICATE = 0
sysCRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 0
sysCRED_TYPE_GENERIC_CERTIFICATE = 0
sysCRED_TYPE_DOMAIN_EXTENDED = 0
sysERROR_NOT_FOUND = syscall.Errno(1)
sysERROR_INVALID_PARAMETER = syscall.Errno(1)
)
func sysCredRead(...interface{}) (*Credential, error) {
return nil, errors.New("Operation not supported")
}
func sysCredWrite(...interface{}) error {
return errors.New("Operation not supported")
}
func sysCredDelete(...interface{}) error {
return errors.New("Operation not supported")
}
func sysCredEnumerate(...interface{}) ([]*Credential, error) {
return nil, errors.New("Operation not supported")
}
+34 -2
View File
@@ -4,19 +4,38 @@ import (
"time" "time"
) )
// CredentialPersistence describes one of three persistence modes of a credential.
// A detailed description of the available modes can be found on
// Docs: https://docs.microsoft.com/en-us/windows/desktop/api/wincred/ns-wincred-_credentialw
type CredentialPersistence uint32 type CredentialPersistence uint32
const ( const (
PersistSession CredentialPersistence = 0x1 // PersistSession indicates that the credential only persists for the life
// of the current Windows login session. Such a credential is not visible in
// any other logon session, even from the same user.
PersistSession CredentialPersistence = 0x1
// PersistLocalMachine indicates that the credential persists for this and
// all subsequent logon sessions on this local machine/computer. It is
// however not visible for logon sessions of this user on a different
// machine.
PersistLocalMachine CredentialPersistence = 0x2 PersistLocalMachine CredentialPersistence = 0x2
PersistEnterprise CredentialPersistence = 0x3
// PersistEnterprise indicates that the credential persists for this and all
// subsequent logon sessions for this user. It is also visible for logon
// sessions on different computers.
PersistEnterprise CredentialPersistence = 0x3
) )
// CredentialAttribute represents an application-specific attribute of a credential.
type CredentialAttribute struct { type CredentialAttribute struct {
Keyword string Keyword string
Value []byte Value []byte
} }
// Credential is the basic credential structure.
// A credential is identified by its target name.
// The actual credential secret is available in the CredentialBlob field.
type Credential struct { type Credential struct {
TargetName string TargetName string
Comment string Comment string
@@ -28,10 +47,23 @@ type Credential struct {
Persist CredentialPersistence Persist CredentialPersistence
} }
// GenericCredential holds a credential for generic usage.
// It is typically defined and used by applications that need to manage user
// secrets.
//
// More information about the available kinds of credentials of the Windows
// Credential Management API can be found on Docs:
// https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/kinds-of-credentials
type GenericCredential struct { type GenericCredential struct {
Credential Credential
} }
// DomainPassword holds a domain credential that is typically used by the
// operating system for user logon.
//
// More information about the available kinds of credentials of the Windows
// Credential Management API can be found on Docs:
// https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/kinds-of-credentials
type DomainPassword struct { type DomainPassword struct {
Credential Credential
} }
+53 -22
View File
@@ -1,19 +1,36 @@
// Package wincred provides primitives for accessing the Windows Credentials Management API.
// This includes functions for retrieval, listing and storage of credentials as well as Go structures for convenient access to the credential data.
//
// A more detailed description of Windows Credentials Management can be found on
// Docs: https://docs.microsoft.com/en-us/windows/desktop/SecAuthN/credentials-management
package wincred package wincred
import ( import "errors"
"syscall"
const (
// ErrElementNotFound is the error that is returned if a requested element cannot be found.
// This error constant can be used to check if a credential could not be found.
ErrElementNotFound = sysERROR_NOT_FOUND
// ErrInvalidParameter is the error that is returned for invalid parameters.
// This error constant can be used to check if the given function parameters were invalid.
// For example when trying to create a new generic credential with an empty target name.
ErrInvalidParameter = sysERROR_INVALID_PARAMETER
) )
// Get the generic credential with the given name from Windows credential manager // GetGenericCredential fetches the generic credential with the given name from Windows credential manager.
// It returns nil and an error if the credential could not be found or an error occurred.
func GetGenericCredential(targetName string) (*GenericCredential, error) { func GetGenericCredential(targetName string) (*GenericCredential, error) {
cred, err := nativeCredRead(targetName, naCRED_TYPE_GENERIC) cred, err := sysCredRead(targetName, sysCRED_TYPE_GENERIC)
if cred != nil { if cred != nil {
return &GenericCredential{*cred}, err return &GenericCredential{*cred}, err
} }
return nil, err return nil, err
} }
// Create a new generic credential with the given name // NewGenericCredential creates a new generic credential object with the given name.
// The persist mode of the newly created object is set to a default value that indicates local-machine-wide storage.
// The credential object is NOT yet persisted to the Windows credential vault.
func NewGenericCredential(targetName string) (result *GenericCredential) { func NewGenericCredential(targetName string) (result *GenericCredential) {
result = new(GenericCredential) result = new(GenericCredential)
result.TargetName = targetName result.TargetName = targetName
@@ -21,28 +38,31 @@ func NewGenericCredential(targetName string) (result *GenericCredential) {
return return
} }
// Persist the credential to Windows credential manager // Write persists the generic credential object to Windows credential manager.
func (t *GenericCredential) Write() (err error) { func (t *GenericCredential) Write() (err error) {
err = nativeCredWrite(&t.Credential, naCRED_TYPE_GENERIC) err = sysCredWrite(&t.Credential, sysCRED_TYPE_GENERIC)
return return
} }
// Delete the credential from Windows credential manager // Delete removes the credential object from Windows credential manager.
func (t *GenericCredential) Delete() (err error) { func (t *GenericCredential) Delete() (err error) {
err = nativeCredDelete(&t.Credential, naCRED_TYPE_GENERIC) err = sysCredDelete(&t.Credential, sysCRED_TYPE_GENERIC)
return return
} }
// Get the domain password credential with the given target host name // GetDomainPassword fetches the domain-password credential with the given target host name from Windows credential manager.
// It returns nil and an error if the credential could not be found or an error occurred.
func GetDomainPassword(targetName string) (*DomainPassword, error) { func GetDomainPassword(targetName string) (*DomainPassword, error) {
cred, err := nativeCredRead(targetName, naCRED_TYPE_DOMAIN_PASSWORD) cred, err := sysCredRead(targetName, sysCRED_TYPE_DOMAIN_PASSWORD)
if cred != nil { if cred != nil {
return &DomainPassword{*cred}, err return &DomainPassword{*cred}, err
} }
return nil, err return nil, err
} }
// Create a new domain password credential used for login to the given target host name // NewDomainPassword creates a new domain-password credential used for login to the given target host name.
// The persist mode of the newly created object is set to a default value that indicates local-machine-wide storage.
// The credential object is NOT yet persisted to the Windows credential vault.
func NewDomainPassword(targetName string) (result *DomainPassword) { func NewDomainPassword(targetName string) (result *DomainPassword) {
result = new(DomainPassword) result = new(DomainPassword)
result.TargetName = targetName result.TargetName = targetName
@@ -50,28 +70,39 @@ func NewDomainPassword(targetName string) (result *DomainPassword) {
return return
} }
// Persist the domain password credential to Windows credential manager // Write persists the domain-password credential to Windows credential manager.
func (t *DomainPassword) Write() (err error) { func (t *DomainPassword) Write() (err error) {
err = nativeCredWrite(&t.Credential, naCRED_TYPE_DOMAIN_PASSWORD) err = sysCredWrite(&t.Credential, sysCRED_TYPE_DOMAIN_PASSWORD)
return return
} }
// Delete the domain password credential from Windows credential manager // Delete removes the domain-password credential from Windows credential manager.
func (t *DomainPassword) Delete() (err error) { func (t *DomainPassword) Delete() (err error) {
err = nativeCredDelete(&t.Credential, naCRED_TYPE_DOMAIN_PASSWORD) err = sysCredDelete(&t.Credential, sysCRED_TYPE_DOMAIN_PASSWORD)
return return
} }
// Set the CredentialBlob field of a domain password credential // SetPassword sets the CredentialBlob field of a domain password credential to the given string.
// using an UTF16 encoded password string
func (t *DomainPassword) SetPassword(pw string) { func (t *DomainPassword) SetPassword(pw string) {
t.CredentialBlob = utf16ToByte(syscall.StringToUTF16(pw)) t.CredentialBlob = utf16ToByte(utf16FromString(pw))
} }
// List the contents of the Credentials store // List retrieves all credentials of the Credentials store.
func List() ([]*Credential, error) { func List() ([]*Credential, error) {
creds, err := nativeCredEnumerate("", true) creds, err := sysCredEnumerate("", true)
if err != nil && err.Error() == naERROR_NOT_FOUND { if err != nil && errors.Is(err, ErrElementNotFound) {
// Ignore ERROR_NOT_FOUND and return an empty list instead
creds = []*Credential{}
err = nil
}
return creds, err
}
// FilteredList retrieves the list of credentials from the Credentials store that match the given filter.
// The filter string defines the prefix followed by an asterisk for the `TargetName` attribute of the credentials.
func FilteredList(filter string) ([]*Credential, error) {
creds, err := sysCredEnumerate(filter, false)
if err != nil && errors.Is(err, ErrElementNotFound) {
// Ignore ERROR_NOT_FOUND and return an empty list instead // Ignore ERROR_NOT_FOUND and return an empty list instead
creds = []*Credential{} creds = []*Credential{}
err = nil err = nil
+3
View File
@@ -0,0 +1,3 @@
# This source code refers to The Go Authors for copyright purposes.
# The master list of authors is in the main Go distribution,
# visible at http://tip.golang.org/AUTHORS.
+3
View File
@@ -0,0 +1,3 @@
# This source code was written by the Go contributors.
# The master list of contributors is in the main Go distribution,
# visible at http://tip.golang.org/CONTRIBUTORS.
+27
View File
@@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+22
View File
@@ -0,0 +1,22 @@
Additional IP Rights Grant (Patents)
"This implementation" means the copyrightable works distributed by
Google as part of the Go project.
Google hereby grants to You a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable (except as stated in this section)
patent license to make, have made, use, offer to sell, sell, import,
transfer and otherwise run, modify and propagate the contents of this
implementation of Go, where such license applies only to those patent
claims, both currently owned or controlled by Google and acquired in
the future, licensable by Google that are necessarily infringed by this
implementation of Go. This grant does not include claims that would be
infringed only as a consequence of further modification of this
implementation. If you or your agent or exclusive licensee institute or
order or agree to the institution of patent litigation against any
entity (including a cross-claim or counterclaim in a lawsuit) alleging
that this implementation of Go or any code incorporated within this
implementation of Go constitutes direct or contributory patent
infringement, or inducement of patent infringement, then any patent
rights granted to you under this License for this implementation of Go
shall terminate as of the date such litigation is filed.
+102
View File
@@ -0,0 +1,102 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package execabs is a drop-in replacement for os/exec
// that requires PATH lookups to find absolute paths.
// That is, execabs.Command("cmd") runs the same PATH lookup
// as exec.Command("cmd"), but if the result is a path
// which is relative, the Run and Start methods will report
// an error instead of running the executable.
//
// See https://blog.golang.org/path-security for more information
// about when it may be necessary or appropriate to use this package.
package execabs
import (
"context"
"fmt"
"os/exec"
"path/filepath"
"reflect"
"unsafe"
)
// ErrNotFound is the error resulting if a path search failed to find an executable file.
// It is an alias for exec.ErrNotFound.
var ErrNotFound = exec.ErrNotFound
// Cmd represents an external command being prepared or run.
// It is an alias for exec.Cmd.
type Cmd = exec.Cmd
// Error is returned by LookPath when it fails to classify a file as an executable.
// It is an alias for exec.Error.
type Error = exec.Error
// An ExitError reports an unsuccessful exit by a command.
// It is an alias for exec.ExitError.
type ExitError = exec.ExitError
func relError(file, path string) error {
return fmt.Errorf("%s resolves to executable in current directory (.%c%s)", file, filepath.Separator, path)
}
// LookPath searches for an executable named file in the directories
// named by the PATH environment variable. If file contains a slash,
// it is tried directly and the PATH is not consulted. The result will be
// an absolute path.
//
// LookPath differs from exec.LookPath in its handling of PATH lookups,
// which are used for file names without slashes. If exec.LookPath's
// PATH lookup would have returned an executable from the current directory,
// LookPath instead returns an error.
func LookPath(file string) (string, error) {
path, err := exec.LookPath(file)
if err != nil {
return "", err
}
if filepath.Base(file) == file && !filepath.IsAbs(path) {
return "", relError(file, path)
}
return path, nil
}
func fixCmd(name string, cmd *exec.Cmd) {
if filepath.Base(name) == name && !filepath.IsAbs(cmd.Path) {
// exec.Command was called with a bare binary name and
// exec.LookPath returned a path which is not absolute.
// Set cmd.lookPathErr and clear cmd.Path so that it
// cannot be run.
lookPathErr := (*error)(unsafe.Pointer(reflect.ValueOf(cmd).Elem().FieldByName("lookPathErr").Addr().Pointer()))
if *lookPathErr == nil {
*lookPathErr = relError(name, cmd.Path)
}
cmd.Path = ""
}
}
// CommandContext is like Command but includes a context.
//
// The provided context is used to kill the process (by calling os.Process.Kill)
// if the context becomes done before the command completes on its own.
func CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd {
cmd := exec.CommandContext(ctx, name, arg...)
fixCmd(name, cmd)
return cmd
}
// Command returns the Cmd struct to execute the named program with the given arguments.
// See exec.Command for most details.
//
// Command differs from exec.Command in its handling of PATH lookups,
// which are used when the program name contains no slashes.
// If exec.Command would have returned an exec.Cmd configured to run an
// executable from the current directory, Command instead
// returns an exec.Cmd that will return an error from Start or Run.
func Command(name string, arg ...string) *exec.Cmd {
cmd := exec.Command(name, arg...)
fixCmd(name, cmd)
return cmd
}
+4
View File
@@ -0,0 +1,4 @@
# github.com/danieljoos/wincred v1.1.0
github.com/danieljoos/wincred
# golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4
golang.org/x/sys/execabs
+101 -4
View File
@@ -1,8 +1,13 @@
package wincred package wincred
import ( import (
"bytes"
"net/url"
"strings"
winc "github.com/danieljoos/wincred" winc "github.com/danieljoos/wincred"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/registryurl"
) )
// Wincred handles secrets using the Windows credential service. // Wincred handles secrets using the Windows credential service.
@@ -10,10 +15,13 @@ type Wincred struct{}
// Add adds new credentials to the windows credentials manager. // Add adds new credentials to the windows credentials manager.
func (h Wincred) Add(creds *credentials.Credentials) error { func (h Wincred) Add(creds *credentials.Credentials) error {
credsLabels := []byte(credentials.CredsLabel)
g := winc.NewGenericCredential(creds.ServerURL) g := winc.NewGenericCredential(creds.ServerURL)
g.UserName = creds.Username g.UserName = creds.Username
g.CredentialBlob = []byte(creds.Secret) g.CredentialBlob = []byte(creds.Secret)
g.Persist = winc.PersistLocalMachine g.Persist = winc.PersistLocalMachine
g.Attributes = []winc.CredentialAttribute{{Keyword: "label", Value: credsLabels}}
return g.Write() return g.Write()
} }
@@ -31,14 +39,94 @@ func (h Wincred) Delete(serverURL string) error {
// Get retrieves credentials from the windows credentials manager. // Get retrieves credentials from the windows credentials manager.
func (h Wincred) Get(serverURL string) (string, string, error) { func (h Wincred) Get(serverURL string) (string, string, error) {
g, _ := winc.GetGenericCredential(serverURL) target, err := getTarget(serverURL)
if err != nil {
return "", "", err
} else if target == "" {
return "", "", credentials.NewErrCredentialsNotFound()
}
g, _ := winc.GetGenericCredential(target)
if g == nil { if g == nil {
return "", "", credentials.NewErrCredentialsNotFound() return "", "", credentials.NewErrCredentialsNotFound()
} }
return g.UserName, string(g.CredentialBlob), nil
for _, attr := range g.Attributes {
if strings.Compare(attr.Keyword, "label") == 0 &&
bytes.Compare(attr.Value, []byte(credentials.CredsLabel)) == 0 {
return g.UserName, string(g.CredentialBlob), nil
}
}
return "", "", credentials.NewErrCredentialsNotFound()
} }
// List returns the stored URLs and corresponding usernames. func getTarget(serverURL string) (string, error) {
s, err := registryurl.Parse(serverURL)
if err != nil {
return serverURL, nil
}
creds, err := winc.List()
if err != nil {
return "", err
}
var targets []string
for i := range creds {
attrs := creds[i].Attributes
for _, attr := range attrs {
if attr.Keyword == "label" && bytes.Equal(attr.Value, []byte(credentials.CredsLabel)) {
targets = append(targets, creds[i].TargetName)
}
}
}
if target, found := findMatch(s, targets, exactMatch); found {
return target, nil
}
if target, found := findMatch(s, targets, approximateMatch); found {
return target, nil
}
return "", nil
}
func findMatch(serverUrl *url.URL, targets []string, matches func(url.URL, url.URL) bool) (string, bool) {
for _, target := range targets {
tURL, err := registryurl.Parse(target)
if err != nil {
continue
}
if matches(*serverUrl, *tURL) {
return target, true
}
}
return "", false
}
func exactMatch(serverURL, target url.URL) bool {
return serverURL.String() == target.String()
}
func approximateMatch(serverURL, target url.URL) bool {
//if scheme is missing assume it is the same as target
if serverURL.Scheme == "" {
serverURL.Scheme = target.Scheme
}
//if port is missing assume it is the same as target
if serverURL.Port() == "" && target.Port() != "" {
serverURL.Host = serverURL.Host + ":" + target.Port()
}
//if path is missing assume it is the same as target
if serverURL.Path == "" {
serverURL.Path = target.Path
}
return serverURL.String() == target.String()
}
// List returns the stored URLs and corresponding usernames for a given credentials label.
func (h Wincred) List() (map[string]string, error) { func (h Wincred) List() (map[string]string, error) {
creds, err := winc.List() creds, err := winc.List()
if err != nil { if err != nil {
@@ -47,7 +135,16 @@ func (h Wincred) List() (map[string]string, error) {
resp := make(map[string]string) resp := make(map[string]string)
for i := range creds { for i := range creds {
resp[creds[i].TargetName] = creds[i].UserName attrs := creds[i].Attributes
for _, attr := range attrs {
if strings.Compare(attr.Keyword, "label") == 0 &&
bytes.Compare(attr.Value, []byte(credentials.CredsLabel)) == 0 {
resp[creds[i].TargetName] = creds[i].UserName
}
}
} }
return resp, nil return resp, nil
} }
+176 -1
View File
@@ -1,6 +1,8 @@
package wincred package wincred
import ( import (
"fmt"
"strings"
"testing" "testing"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
@@ -19,6 +21,31 @@ func TestWinCredHelper(t *testing.T) {
} }
helper := Wincred{} helper := Wincred{}
// check for and remove remaining credentials from previous fail tests
oldauths, err := helper.List()
if err != nil {
t.Fatal(err)
}
for k, v := range oldauths {
if strings.Compare(k, creds.ServerURL) == 0 && strings.Compare(v, creds.Username) == 0 {
if err := helper.Delete(creds.ServerURL); err != nil {
t.Fatal(err)
}
} else if strings.Compare(k, creds1.ServerURL) == 0 && strings.Compare(v, creds1.Username) == 0 {
if err := helper.Delete(creds1.ServerURL); err != nil {
t.Fatal(err)
}
}
}
// recount for credentials
oldauths, err = helper.List()
if err != nil {
t.Fatal(err)
}
if err := helper.Add(creds); err != nil { if err := helper.Add(creds); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -37,7 +64,7 @@ func TestWinCredHelper(t *testing.T) {
} }
auths, err := helper.List() auths, err := helper.List()
if err != nil || len(auths) == 0 { if err != nil || len(auths)-len(oldauths) != 1 {
t.Fatal(err) t.Fatal(err)
} }
@@ -60,6 +87,154 @@ func TestWinCredHelper(t *testing.T) {
} }
} }
// TestWinCredHelperRetrieveAliases verifies that secrets can be accessed
// through variations on the URL
func TestWinCredHelperRetrieveAliases(t *testing.T) {
tests := []struct {
storeURL string
readURL string
}{
// stored with port, retrieved without
{"https://foobar.docker.io:2376", "https://foobar.docker.io"},
// stored as https, retrieved without scheme
{"https://foobar.docker.io", "foobar.docker.io"},
// stored with path, retrieved without
{"https://foobar.docker.io/one/two", "https://foobar.docker.io"},
}
helper := Wincred{}
defer func() {
for _, te := range tests {
helper.Delete(te.storeURL)
}
}()
// Clean store before testing.
for _, te := range tests {
helper.Delete(te.storeURL)
}
for _, te := range tests {
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
continue
}
if _, _, err := helper.Get(te.readURL); err != nil {
t.Errorf("Error: failed to read secret for URL %q using %q", te.storeURL, te.readURL)
}
helper.Delete(te.storeURL)
}
}
// TestWinCredHelperRetrieveStrict verifies that only matching secrets are
// returned.
func TestWinCredHelperRetrieveStrict(t *testing.T) {
tests := []struct {
storeURL string
readURL string
}{
// stored as https, retrieved using http
{"https://foobar.docker.io:2376", "http://foobar.docker.io:2376"},
// stored as http, retrieved using https
{"http://foobar.docker.io:2376", "https://foobar.docker.io:2376"},
// same: stored as http, retrieved without a scheme specified (hence, using the default https://)
{"http://foobar.docker.io", "foobar.docker.io:5678"},
// non-matching ports
{"https://foobar.docker.io:1234", "https://foobar.docker.io:5678"},
// non-matching ports TODO is this desired behavior? The other way round does work
//{"https://foobar.docker.io", "https://foobar.docker.io:5678"},
// non-matching paths
{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234/five/six"},
}
helper := Wincred{}
defer func() {
for _, te := range tests {
helper.Delete(te.storeURL)
}
}()
// Clean store before testing.
for _, te := range tests {
helper.Delete(te.storeURL)
}
for _, te := range tests {
c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
continue
}
if _, _, err := helper.Get(te.readURL); err == nil {
t.Errorf("Error: managed to read secret for URL %q using %q, but should not be able to", te.storeURL, te.readURL)
}
helper.Delete(te.storeURL)
}
}
// TestWinCredHelperStoreRetrieve verifies that secrets stored in the
// the keychain can be read back using the URL that was used to store them.
func TestWinCredHelperStoreRetrieve(t *testing.T) {
tests := []struct {
url string
}{
{url: "foobar.docker.io"},
{url: "foobar.docker.io:2376"},
{url: "//foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376"},
{url: "http://foobar.docker.io:2376"},
{url: "https://foobar.docker.io:2376/some/path"},
{url: "https://foobar.docker.io:2376/some/other/path"},
{url: "https://foobar.docker.io:2376/some/other/path?foo=bar"},
}
helper := Wincred{}
defer func() {
for _, te := range tests {
helper.Delete(te.url)
}
}()
// Clean store before testing.
for _, te := range tests {
helper.Delete(te.url)
}
// Note that we don't delete between individual tests here, to verify that
// subsequent stores/overwrites don't affect storing / retrieving secrets.
for i, te := range tests {
c := &credentials.Credentials{
ServerURL: te.url,
Username: fmt.Sprintf("user-%d", i),
Secret: fmt.Sprintf("secret-%d", i),
}
if err := helper.Add(c); err != nil {
t.Errorf("Error: failed to store secret for URL: %s: %s", te.url, err)
continue
}
user, secret, err := helper.Get(te.url)
if err != nil {
t.Errorf("Error: failed to read secret for URL %q: %s", te.url, err)
continue
}
if user != c.Username {
t.Errorf("Error: expected username %s, got username %s for URL: %s", c.Username, user, te.url)
}
if secret != c.Secret {
t.Errorf("Error: expected secret %s, got secret %s for URL: %s", c.Secret, secret, te.url)
}
}
}
func TestMissingCredentials(t *testing.T) { func TestMissingCredentials(t *testing.T) {
helper := Wincred{} helper := Wincred{}
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd") _, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")