1
0
mirror of https://github.com/docker/docker-credential-helpers.git synced 2026-06-28 07:11:36 +05:30

Compare commits

...

42 Commits

Author SHA1 Message Date
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
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
20 changed files with 501 additions and 255 deletions
+4 -3
View File
@@ -3,20 +3,21 @@
sudo: required
language: go
dist: trusty
osx_image: xcode10.1
os:
- linux
- osx
notifications:
email: false
go:
- 1.8
install: make deps
- 1.12.x
addons:
apt:
packages:
- libsecret-1-dev
- pass
before_script:
- make deps
- "export DISPLAY=:99.0"
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sh ci/before_script_linux.sh; fi
- make validate
@@ -28,7 +29,7 @@
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="
secure: "$GITHUB_TOKEN"
# 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
Vendored
+81
View File
@@ -0,0 +1,81 @@
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'
archiveArtifacts 'bin/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'
archiveArtifacts 'bin/docker-credential-*'
}
}
}
stage('windows') {
agent {
label 'win-build && go1.12.4'
}
environment {
GOPATH = pwd()
PATH = "${pwd()}/bin;$PATH"
PFX = credentials('windows-build-pfx-sanitize')
PFXPASSWORD = credentials('windows-build-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'
}
}
}
}
}
}
}
-30
View File
@@ -11,18 +11,13 @@
[Org]
[Org."Core maintainers"]
people = [
"aaronlehmann",
"calavera",
"coolljt0725",
"cpuguy83",
"crosbymichael",
"dnephin",
"dongluochen",
"duglin",
"estesp",
"icecrime",
"jhowardmsft",
"lk4d4",
"mavenugo",
"mhbauer",
"n4ss",
@@ -45,16 +40,6 @@
# 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]
Name = "Lei Jitang"
Email = "leijitang@huawei.com"
@@ -75,11 +60,6 @@
Email = "dnephin@gmail.com"
GitHub = "dnephin"
[people.dongluochen]
Name = "Dongluo Chen"
Email = "dongluo.chen@docker.com"
GitHub = "dongluochen"
[people.duglin]
Name = "Doug Davis"
Email = "dug@us.ibm.com"
@@ -90,21 +70,11 @@
Email = "estesp@linux.vnet.ibm.com"
GitHub = "estesp"
[people.icecrime]
Name = "Arnaud Porterie"
Email = "arnaud@docker.com"
GitHub = "icecrime"
[people.jhowardmsft]
Name = "John Howard"
Email = "jhoward@microsoft.com"
GitHub = "jhowardmsft"
[people.lk4d4]
Name = "Alexander Morozov"
Email = "lk4d4@docker.com"
GitHub = "lk4d4"
[people.mavenugo]
Name = "Madhu Venugopal"
Email = "madhu@docker.com"
+1 -1
View File
@@ -6,7 +6,7 @@ VERSION := $(shell grep 'const Version' credentials/version.go | awk -F'"' '{ pr
all: test
deps:
go get -u github.com/golang/lint/golint
go get -u golang.org/x/lint/golint
clean:
rm -rf bin
+1 -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
```
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
+2 -2
View File
@@ -2,13 +2,13 @@ image: Visual Studio 2015
environment:
GOPATH: c:\gopath
stack: go 1.8.7
clone_folder: c:\gopath\src\github.com\docker\docker-credential-helpers
clone_depth: 10
before_build:
- set PATH=%PATH%;C:\MinGW\bin;
- set PATH=%PATH%;C:\go18\bin;
- set GOROOT=C:\go18
build_script:
- mingw32-make vet_win wincred
+1 -1
View File
@@ -1,4 +1,4 @@
package credentials
// Version holds a string describing the current version
const Version = "0.6.0"
const Version = "0.6.3"
-1
View File
@@ -224,5 +224,4 @@ void freeListData(char *** data, unsigned int length) {
for(int i=0; i<length; i++) {
free((*data)[i]);
}
free(*data);
}
+12 -39
View File
@@ -1,8 +1,8 @@
package osxkeychain
/*
#cgo CFLAGS: -x objective-c -mmacosx-version-min=10.10
#cgo LDFLAGS: -framework Security -framework Foundation -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.11
#include "osxkeychain_darwin.h"
#include <stdlib.h>
@@ -10,12 +10,11 @@ package osxkeychain
import "C"
import (
"errors"
"net/url"
"strconv"
"strings"
"unsafe"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/registryurl"
)
// errCredentialsNotFound is the specific error message returned by OS X
@@ -110,15 +109,18 @@ func (h Osxkeychain) List() (map[string]string, error) {
defer C.free(unsafe.Pointer(acctsC))
var listLenC C.uint
errMsg := C.keychain_list(credsLabelC, &pathsC, &acctsC, &listLenC)
defer C.freeListData(&pathsC, listLenC)
defer C.freeListData(&acctsC, listLenC)
if errMsg != nil {
defer C.free(unsafe.Pointer(errMsg))
goMsg := C.GoString(errMsg)
if goMsg == errCredentialsNotFound {
return make(map[string]string), nil
}
return nil, errors.New(goMsg)
}
defer C.freeListData(&pathsC, listLenC)
defer C.freeListData(&acctsC, listLenC)
var listLen int
listLen = int(listLenC)
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
@@ -135,7 +137,7 @@ func (h Osxkeychain) List() (map[string]string, error) {
}
func splitServer(serverURL string) (*C.struct_Server, error) {
u, err := parseURL(serverURL)
u, err := registryurl.Parse(serverURL)
if err != nil {
return nil, err
}
@@ -145,7 +147,7 @@ func splitServer(serverURL string) (*C.struct_Server, error) {
proto = C.kSecProtocolTypeHTTP
}
var port int
p := getPort(u)
p := registryurl.GetPort(u)
if p != "" {
port, err = strconv.Atoi(p)
if err != nil {
@@ -155,7 +157,7 @@ func splitServer(serverURL string) (*C.struct_Server, error) {
return &C.struct_Server{
proto: C.SecProtocolType(proto),
host: C.CString(getHostname(u)),
host: C.CString(registryurl.GetHostname(u)),
port: C.uint(port),
path: C.CString(u.Path),
}, nil
@@ -165,32 +167,3 @@ func freeServer(s *C.struct_Server) {
C.free(unsafe.Pointer(s.host))
C.free(unsafe.Pointer(s.path))
}
// parseURL 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 parseURL(serverURL string) (*url.URL, error) {
// Check if serverURL has a scheme, otherwise add `//` as scheme.
if !strings.Contains(serverURL, "://") && !strings.HasPrefix(serverURL, "//") {
serverURL = "//" + serverURL
}
u, err := url.Parse(serverURL)
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
}
+2 -42
View File
@@ -1,10 +1,10 @@
package osxkeychain
import (
"errors"
"fmt"
"github.com/docker/docker-credential-helpers/credentials"
"testing"
"github.com/docker/docker-credential-helpers/credentials"
)
func TestOSXKeychainHelper(t *testing.T) {
@@ -56,46 +56,6 @@ func TestOSXKeychainHelper(t *testing.T) {
}
}
// TestOSXKeychainHelperParseURL verifies that a // "scheme" is added to URLs,
// and that invalid URLs produce an error.
func TestOSXKeychainHelperParseURL(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 := parseURL(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)
}
}
}
// TestOSXKeychainHelperRetrieveAliases verifies that secrets can be accessed
// through variations on the URL
func TestOSXKeychainHelperRetrieveAliases(t *testing.T) {
-13
View File
@@ -1,13 +0,0 @@
//+build go1.8
package osxkeychain
import "net/url"
func getHostname(u *url.URL) string {
return u.Hostname()
}
func getPort(u *url.URL) string {
return u.Port()
}
+64 -111
View File
@@ -5,6 +5,7 @@
package pass
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
@@ -13,139 +14,99 @@ import (
"os/exec"
"path"
"strings"
"sync"
"github.com/docker/docker-credential-helpers/credentials"
)
const PASS_FOLDER = "docker-credential-helpers"
var (
PassInitialized bool
)
func init() {
// In principle, we could just run `pass init`. However, pass has a bug
// where if gpg fails, it doesn't always exit 1. Additionally, pass
// uses gpg2, but gpg is the default, which may be confusing. So let's
// just explictily check that pass actually can store and retreive a
// password.
password := "pass is initialized"
name := path.Join(PASS_FOLDER, "docker-pass-initialized-check")
_, err := runPass(password, "insert", "-f", "-m", name)
if err != nil {
return
}
stored, err := runPass("", "show", name)
PassInitialized = err == nil && stored == password
if PassInitialized {
runPass("", "rm", "-rf", name)
}
}
func runPass(stdinContent string, args ...string) (string, error) {
cmd := exec.Command("pass", args...)
stdin, err := cmd.StdinPipe()
if err != nil {
return "", err
}
defer stdin.Close()
stderr, err := cmd.StderrPipe()
if err != nil {
return "", err
}
defer stderr.Close()
stdout, err := cmd.StdoutPipe()
if err != nil {
return "", err
}
defer stdout.Close()
err = cmd.Start()
if err != nil {
return "", err
}
_, err = stdin.Write([]byte(stdinContent))
if err != nil {
return "", err
}
stdin.Close()
errContent, err := ioutil.ReadAll(stderr)
if err != nil {
return "", fmt.Errorf("error reading stderr: %s", err)
}
result, err := ioutil.ReadAll(stdout)
if err != nil {
return "", fmt.Errorf("Error reading stdout: %s", err)
}
cmdErr := cmd.Wait()
if cmdErr != nil {
return "", fmt.Errorf("%s: %s", cmdErr, errContent)
}
return string(result), nil
}
// Pass handles secrets using Linux secret-service as a store.
type Pass struct{}
// Add adds new credentials to the keychain.
func (h Pass) Add(creds *credentials.Credentials) error {
if !PassInitialized {
return errors.New("pass store is uninitialized")
// 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 (h Pass) Add(creds *credentials.Credentials) error {
if creds == nil {
return errors.New("missing credentials")
}
encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL))
_, err := runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
_, err := h.runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
return err
}
// Delete removes credentials from the store.
func (h Pass) Delete(serverURL string) error {
if !PassInitialized {
return errors.New("pass store is uninitialized")
}
if serverURL == "" {
return errors.New("missing server url")
}
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
_, err := runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
_, err := h.runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
return err
}
func getPassDir() string {
passDir := os.ExpandEnv("$HOME/.password-store")
for _, e := range os.Environ() {
parts := strings.SplitN(e, "=", 2)
if len(parts) < 2 {
continue
}
if parts[0] != "PASSWORD_STORE_DIR" {
continue
}
passDir = parts[1]
break
passDir := "$HOME/.password-store"
if envDir := os.Getenv("PASSWORD_STORE_DIR"); envDir != "" {
passDir = envDir
}
return passDir
return os.ExpandEnv(passDir)
}
// listPassDir lists all the contents of a directory in the password store.
@@ -168,10 +129,6 @@ func listPassDir(args ...string) ([]os.FileInfo, error) {
// Get returns the username and secret to use for a given registry server URL.
func (h Pass) Get(serverURL string) (string, string, error) {
if !PassInitialized {
return "", "", errors.New("pass store is uninitialized")
}
if serverURL == "" {
return "", "", errors.New("missing server url")
}
@@ -180,7 +137,7 @@ func (h Pass) Get(serverURL string) (string, string, error) {
if _, err := os.Stat(path.Join(getPassDir(), PASS_FOLDER, encoded)); err != nil {
if os.IsNotExist(err) {
return "", "", nil;
return "", "", nil
}
return "", "", err
@@ -196,16 +153,12 @@ func (h Pass) Get(serverURL string) (string, string, error) {
}
actual := strings.TrimSuffix(usernames[0].Name(), ".gpg")
secret, err := runPass("", "show", path.Join(PASS_FOLDER, encoded, actual))
secret, err := h.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 (h Pass) List() (map[string]string, error) {
if !PassInitialized {
return nil, errors.New("pass store is uninitialized")
}
servers, err := listPassDir()
if err != nil {
return nil, err
+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)
}
}
}
+15
View File
@@ -0,0 +1,15 @@
//+build go1.8
package registryurl
import (
url "net/url"
)
func GetHostname(u *url.URL) string {
return u.Hostname()
}
func GetPort(u *url.URL) string {
return u.Port()
}
@@ -1,17 +1,17 @@
//+build !go1.8
package osxkeychain
package registryurl
import (
"net/url"
url "net/url"
"strings"
)
func getHostname(u *url.URL) string {
func GetHostname(u *url.URL) string {
return stripPort(u.Host)
}
func getPort(u *url.URL) string {
func GetPort(u *url.URL) string {
return portOnly(u.Host)
}
-1
View File
@@ -158,5 +158,4 @@ void freeListData(char *** data, unsigned int length) {
for(i=0; i<length; i++) {
free((*data)[i]);
}
free(*data);
}
+4 -4
View File
@@ -92,12 +92,12 @@ func (h Secretservice) List() (map[string]string, error) {
defer C.free(unsafe.Pointer(acctsC))
var listLenC C.uint
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(&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)
+78 -2
View File
@@ -2,10 +2,12 @@ package wincred
import (
"bytes"
"net/url"
"strings"
winc "github.com/danieljoos/wincred"
"github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/registryurl"
)
// Wincred handles secrets using the Windows credential service.
@@ -13,11 +15,12 @@ type Wincred struct{}
// Add adds new credentials to the windows credentials manager.
func (h Wincred) Add(creds *credentials.Credentials) error {
credsLabels := []byte(credentials.CredsLabel)
g := winc.NewGenericCredential(creds.ServerURL)
g.UserName = creds.Username
g.CredentialBlob = []byte(creds.Secret)
g.Persist = winc.PersistLocalMachine
g.Attributes = []winc.CredentialAttribute{{"label", []byte(credentials.CredsLabel)}}
g.Attributes = []winc.CredentialAttribute{{Keyword: "label", Value: credsLabels}}
return g.Write()
}
@@ -36,10 +39,18 @@ func (h Wincred) Delete(serverURL string) error {
// Get retrieves credentials from the windows credentials manager.
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 {
return "", "", credentials.NewErrCredentialsNotFound()
}
for _, attr := range g.Attributes {
if strings.Compare(attr.Keyword, "label") == 0 &&
bytes.Compare(attr.Value, []byte(credentials.CredsLabel)) == 0 {
@@ -50,6 +61,71 @@ func (h Wincred) Get(serverURL string) (string, string, error) {
return "", "", credentials.NewErrCredentialsNotFound()
}
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) {
creds, err := winc.List()
+149
View File
@@ -1,6 +1,7 @@
package wincred
import (
"fmt"
"strings"
"testing"
@@ -86,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) {
helper := Wincred{}
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")