mirror of
https://github.com/docker/docker-credential-helpers.git
synced 2026-06-28 15:21:29 +05:30
Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fc9290adbc | |||
| 8369960895 | |||
| e4a625d24b | |||
| f552261f32 | |||
| 7a2694fc98 | |||
| 4b9fe97b79 | |||
| 0232333efa | |||
| 38bea2ce27 | |||
| c9a35c136e | |||
| 431b64c703 | |||
| 951b97d7f5 | |||
| f7d32862eb | |||
| 699f1d6790 | |||
| f4b8a8531e | |||
| f76f0b3f33 | |||
| f78081d1f7 | |||
| 4e7d4a98a4 | |||
| 43d1f2919f | |||
| 7b072d4d2e | |||
| c0c41f47b2 | |||
| 69fb197018 | |||
| 86b653a9f1 | |||
| 88f932172b | |||
| 54f0238b6b | |||
| 2bf42cfd47 | |||
| 1c9f7ede70 | |||
| 87c80bfba5 | |||
| 680ca48e6d | |||
| f6d4261609 | |||
| f755519945 | |||
| 11d9f9dba7 | |||
| beda055c57 | |||
| 22b8706efa | |||
| 8a9f93a99f | |||
| 063cca0a6d | |||
| 152d64310b | |||
| 77e30bd9dd | |||
| 74636a1592 | |||
| a3c1b5b757 | |||
| 6f4b0a7c06 | |||
| 1546024a83 | |||
| ecb01138bd | |||
| df92c83808 | |||
| 123ba1b7cd | |||
| 9c18f033f7 | |||
| d6c1f136e4 | |||
| 73e5f5dbfe | |||
| 5241b46610 | |||
| 3cba3913ea | |||
| 8502b53592 | |||
| 5da09fd251 | |||
| dd27c246bd | |||
| 26deb2937d | |||
| a13ff50017 | |||
| 1c295f7de8 | |||
| 093af814ee | |||
| d499cf5cb9 | |||
| b049338a6b | |||
| 91fc39d57a | |||
| 317219f3a6 | |||
| 21f4937ebc | |||
| 19b711cc92 | |||
| 1f635a73ad | |||
| 302e4ae938 | |||
| d68f9aeca3 | |||
| 05a9d4c50d | |||
| c2eec534ee | |||
| 5be80ca212 | |||
| f00de1b72f | |||
| 72f0375e37 | |||
| 3cce61446f | |||
| ec0d036273 | |||
| d3d9934897 | |||
| f212ea17df | |||
| 09e536a128 | |||
| 3c90bd29a4 | |||
| b8fb9690c8 | |||
| 7efaffb4c4 | |||
| 6338c06ba4 | |||
| 86c94d3e30 | |||
| 1ab1037707 | |||
| c69c0725bb | |||
| a8de4f6e8a | |||
| 4fbc86d7d0 | |||
| 2d19ebb7f4 | |||
| 054c53824f | |||
| be1808e3ed | |||
| e1d4c012bc | |||
| ad6ee5d58d | |||
| 94be56b6f4 | |||
| 37bf8afe8b | |||
| 3d7e1817f2 | |||
| 5651367281 | |||
| d8f57a18c6 | |||
| 4bc0bc2a85 | |||
| 167b137eb4 | |||
| 2a67ef1524 | |||
| 6ba4edf6e9 | |||
| fa4a4d4f71 | |||
| 4d60b372aa | |||
| 607bf3c174 | |||
| 74f4f75bc1 | |||
| 0e7779e5a1 | |||
| fb9549d396 | |||
| ed11c58ebf | |||
| 79ab7059b0 | |||
| 18d35e4984 | |||
| cdde659563 | |||
| 71779cf7f5 | |||
| 51c78cdc14 | |||
| 120e37f15d | |||
| 4962f775bf | |||
| ce617b3357 | |||
| f3071aff0a | |||
| 1515d4547e | |||
| f67589c36e | |||
| 479de2a4f5 | |||
| cdba2ced06 | |||
| 7f0538cd5e | |||
| 14381bf0d6 | |||
| 7133af577e | |||
| 2f2e85cfb9 | |||
| 47566329ff | |||
| b9d19b479a | |||
| e522e56699 | |||
| 8cb3338668 | |||
| cd76e4253f | |||
| 021d7d6a19 | |||
| 2a8670e0da | |||
| c5fbd3a5ad | |||
| c6cf8aa13b | |||
| cfe7556d6d | |||
| 23a1f310a5 | |||
| f7f2744e6d | |||
| 406812bf8e | |||
| 595b7f2531 | |||
| ad4463616e | |||
| 365da011fb | |||
| 1057cf7f86 | |||
| 40d06d0090 | |||
| de50f50ab0 | |||
| 3c3e1d3af1 | |||
| 19ec1c3164 | |||
| 2a3f7a4468 | |||
| b7c53e02cd | |||
| 94963d0da8 | |||
| 2514d5e8b2 | |||
| 01ed4b811b | |||
| 9b6be7c243 | |||
| f853612e07 | |||
| 69c9d2eab8 | |||
| eecc09c974 | |||
| 79f93e5e69 | |||
| 48079a964a | |||
| cfbce1c845 | |||
| a994ca1d54 | |||
| f72c04f1d8 | |||
| c45d9e9e28 | |||
| 80833adff5 | |||
| f1498a0524 | |||
| c2abee4c0c | |||
| 9e96a4905d | |||
| cf9b6df432 | |||
| b0c64357eb | |||
| 5b764cc13a | |||
| b63a32e7a2 | |||
| 173fe2dbc2 | |||
| b3ebaa455d | |||
| 029e094488 | |||
| 8fa18eb16c | |||
| 887a66459a | |||
| d2e6ed779a | |||
| 9557dc3c3f | |||
| 59b3d54595 | |||
| 205e3b3056 | |||
| 7566a1e399 | |||
| 72661b3103 | |||
| 5a8fb214ed | |||
| 5128fa1bad | |||
| fee9277aa7 | |||
| d80b179703 | |||
| c4fc9c07dd | |||
| 2c63e775b4 | |||
| 00703eb6db | |||
| df8c7a02f1 | |||
| 4b8917b1cf | |||
| 039c315f22 | |||
| 6bbb56ae3c | |||
| ec937eebe2 | |||
| fa24bb8912 | |||
| 76055c2deb |
@@ -1 +1,2 @@
|
|||||||
bin
|
bin
|
||||||
|
release
|
||||||
|
|||||||
+5
-29
@@ -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+.*$/
|
|
||||||
|
|||||||
@@ -4,6 +4,52 @@ 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)
|
||||||
|
|
||||||
|
- Full implementation for OSX ready
|
||||||
|
- Fix some windows issues
|
||||||
|
- Implement client.List, change list API
|
||||||
|
- mac: delete credentials before adding them to avoid already exist error (fixes #37)
|
||||||
|
|
||||||
|
## v0.3.0 (Go client)
|
||||||
|
|
||||||
|
- Add Go client library to talk with the native programs.
|
||||||
|
|
||||||
## v0.2.0 (Mac OS X, Windows, Linux)
|
## v0.2.0 (Mac OS X, Windows, Linux)
|
||||||
|
|
||||||
- Initial release of docker-credential-secretservice for Linux.
|
- Initial release of docker-credential-secretservice for Linux.
|
||||||
|
|||||||
Vendored
+83
@@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
# docker-credential-helpers maintainers file
|
||||||
|
#
|
||||||
|
# This file describes who runs the docker/docker-credential-helpers project and how.
|
||||||
|
# This is a living document - if you see something out of date or missing, speak up!
|
||||||
|
#
|
||||||
|
# It is structured to be consumable by both humans and programs.
|
||||||
|
# To extract its contents programmatically, use any TOML-compliant parser.
|
||||||
|
#
|
||||||
|
# This file is compiled into the MAINTAINERS file in docker/opensource.
|
||||||
|
#
|
||||||
|
[Org]
|
||||||
|
[Org."Core maintainers"]
|
||||||
|
people = [
|
||||||
|
"coolljt0725",
|
||||||
|
"cpuguy83",
|
||||||
|
"crosbymichael",
|
||||||
|
"dnephin",
|
||||||
|
"duglin",
|
||||||
|
"estesp",
|
||||||
|
"jhowardmsft",
|
||||||
|
"mavenugo",
|
||||||
|
"mhbauer",
|
||||||
|
"n4ss",
|
||||||
|
"runcom",
|
||||||
|
"stevvooe",
|
||||||
|
"thajeztah",
|
||||||
|
"tianon",
|
||||||
|
"tibor",
|
||||||
|
"tonistiigi",
|
||||||
|
"unclejack",
|
||||||
|
"vdemeester",
|
||||||
|
"vieux"
|
||||||
|
]
|
||||||
|
|
||||||
|
[people]
|
||||||
|
|
||||||
|
# A reference list of all people associated with the project.
|
||||||
|
# All other sections should refer to people by their canonical key
|
||||||
|
# in the people section.
|
||||||
|
|
||||||
|
# ADD YOURSELF HERE IN ALPHABETICAL ORDER
|
||||||
|
|
||||||
|
[people.coolljt0725]
|
||||||
|
Name = "Lei Jitang"
|
||||||
|
Email = "leijitang@huawei.com"
|
||||||
|
GitHub = "coolljt0725"
|
||||||
|
|
||||||
|
[people.cpuguy83]
|
||||||
|
Name = "Brian Goff"
|
||||||
|
Email = "cpuguy83@gmail.com"
|
||||||
|
Github = "cpuguy83"
|
||||||
|
|
||||||
|
[people.crosbymichael]
|
||||||
|
Name = "Michael Crosby"
|
||||||
|
Email = "crosbymichael@gmail.com"
|
||||||
|
GitHub = "crosbymichael"
|
||||||
|
|
||||||
|
[people.dnephin]
|
||||||
|
Name = "Daniel Nephin"
|
||||||
|
Email = "dnephin@gmail.com"
|
||||||
|
GitHub = "dnephin"
|
||||||
|
|
||||||
|
[people.duglin]
|
||||||
|
Name = "Doug Davis"
|
||||||
|
Email = "dug@us.ibm.com"
|
||||||
|
GitHub = "duglin"
|
||||||
|
|
||||||
|
[people.estesp]
|
||||||
|
Name = "Phil Estes"
|
||||||
|
Email = "estesp@linux.vnet.ibm.com"
|
||||||
|
GitHub = "estesp"
|
||||||
|
|
||||||
|
[people.jhowardmsft]
|
||||||
|
Name = "John Howard"
|
||||||
|
Email = "jhoward@microsoft.com"
|
||||||
|
GitHub = "jhowardmsft"
|
||||||
|
|
||||||
|
[people.mavenugo]
|
||||||
|
Name = "Madhu Venugopal"
|
||||||
|
Email = "madhu@docker.com"
|
||||||
|
GitHub = "mavenugo"
|
||||||
|
|
||||||
|
[people.mhbauer]
|
||||||
|
Name = "Morgan Bauer"
|
||||||
|
Email = "mbauer@us.ibm.com"
|
||||||
|
GitHub = "mhbauer"
|
||||||
|
|
||||||
|
[people.n4ss]
|
||||||
|
Name = "Nassim Eddequiouaq"
|
||||||
|
Email = "eddequiouaq.nassim@gmail.com"
|
||||||
|
GitHub = "n4ss"
|
||||||
|
|
||||||
|
[people.runcom]
|
||||||
|
Name = "Antonio Murdaca"
|
||||||
|
Email = "runcom@redhat.com"
|
||||||
|
GitHub = "runcom"
|
||||||
|
|
||||||
|
[people.stevvooe]
|
||||||
|
Name = "Stephen Day"
|
||||||
|
Email = "stephen.day@docker.com"
|
||||||
|
GitHub = "stevvooe"
|
||||||
|
|
||||||
|
[people.thajeztah]
|
||||||
|
Name = "Sebastiaan van Stijn"
|
||||||
|
Email = "github@gone.nl"
|
||||||
|
GitHub = "thaJeztah"
|
||||||
|
|
||||||
|
[people.tianon]
|
||||||
|
Name = "Tianon Gravi"
|
||||||
|
Email = "admwiggin@gmail.com"
|
||||||
|
GitHub = "tianon"
|
||||||
|
|
||||||
|
[people.tibor]
|
||||||
|
Name = "Tibor Vass"
|
||||||
|
Email = "tibor@docker.com"
|
||||||
|
GitHub = "tiborvass"
|
||||||
|
|
||||||
|
[people.tonistiigi]
|
||||||
|
Name = "Tõnis Tiigi"
|
||||||
|
Email = "tonis@docker.com"
|
||||||
|
GitHub = "tonistiigi"
|
||||||
|
|
||||||
|
[people.unclejack]
|
||||||
|
Name = "Cristian Staretu"
|
||||||
|
Email = "cristian.staretu@gmail.com"
|
||||||
|
GitHub = "unclejack"
|
||||||
|
|
||||||
|
[people.vdemeester]
|
||||||
|
Name = "Vincent Demeester"
|
||||||
|
Email = "vincent@sbr.pm"
|
||||||
|
GitHub = "vdemeester"
|
||||||
|
|
||||||
|
[people.vieux]
|
||||||
|
Name = "Victor Vieux"
|
||||||
|
Email = "vieux@docker.com"
|
||||||
|
GitHub = "vieux"
|
||||||
@@ -1,23 +1,50 @@
|
|||||||
.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 -o bin/docker-credential-osxkeychain osxkeychain/cmd/main_darwin.go
|
go build -ldflags -s -o bin/docker-credential-osxkeychain osxkeychain/cmd/main_darwin.go
|
||||||
|
|
||||||
|
osxcodesign: osxkeychain
|
||||||
|
$(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 codesign --verify --deep --strict --verbose=2 --display bin/docker-credential-osxkeychain
|
||||||
|
|
||||||
secretservice:
|
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 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
|
||||||
@@ -26,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)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ docker-credential-helpers is a suite of programs to use native stores to keep Do
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Go to the [Releases](https://github.com/calavera/docker-credential-helpers/releases) page and download the binary that works better for you. Put that binary in your `$PATH`, so Docker can find it.
|
Go to the [Releases](https://github.com/docker/docker-credential-helpers/releases) page and download the binary that works better for you. Put that binary in your `$PATH`, so Docker can find it.
|
||||||
|
|
||||||
### Building from scratch
|
### Building from scratch
|
||||||
|
|
||||||
@@ -13,13 +13,13 @@ The programs in this repository are written with the Go programming language. Th
|
|||||||
1 - Download the source and put it in your `$GOPATH` with `go get`.
|
1 - Download the source and put it in your `$GOPATH` with `go get`.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ go get github.com/calavera/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/calavera/docker-credentials-helpers
|
$ cd $GOPATH/docker/docker-credentials-helpers
|
||||||
$ make osxkeychain
|
$ make osxkeychain
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -27,6 +27,8 @@ $ make osxkeychain
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
### With the Docker Engine
|
||||||
|
|
||||||
Set the `credsStore` option in your `.docker/config.json` file with the suffix of the program you want to use. For instance, set it to `osxkeychain` if you want to use `docker-credential-osxkeychain`.
|
Set the `credsStore` option in your `.docker/config.json` file with the suffix of the program you want to use. For instance, set it to `osxkeychain` if you want to use `docker-credential-osxkeychain`.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -35,19 +37,39 @@ Set the `credsStore` option in your `.docker/config.json` file with the suffix o
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### With other command line applications
|
||||||
|
|
||||||
|
The sub-package [client](https://godoc.org/github.com/docker/docker-credential-helpers/client) includes
|
||||||
|
functions to call external programs from your own command line applications.
|
||||||
|
|
||||||
|
There are three things you need to know if you need to interact with a helper:
|
||||||
|
|
||||||
|
1. The name of the program to execute, for instance `docker-credential-osxkeychain`.
|
||||||
|
2. The server address to identify the credentials, for instance `https://example.com`.
|
||||||
|
3. The username and secret to store, when you want to store credentials.
|
||||||
|
|
||||||
|
You can see examples of each function in the [client](https://godoc.org/github.com/docker/docker-credential-helpers/client) documentation.
|
||||||
|
|
||||||
### Available programs
|
### Available programs
|
||||||
|
|
||||||
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.
|
||||||
1. 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.
|
||||||
2. 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
|
||||||
|
|
||||||
A credential helper can be any program that can read values from the standard input. We use the first argument in the command line to differentiate the kind of command to execute. There are three valid values:
|
A credential helper can be any program that can read values from the standard input. We use the first argument in the command line to differentiate the kind of command to execute. There are four valid values:
|
||||||
|
|
||||||
- `store`: Adds credentials to the keychain. The payload in the standard input is a JSON document with `ServerURL`, `Username` and `Secret`.
|
- `store`: Adds credentials to the keychain. The payload in the standard input is a JSON document with `ServerURL`, `Username` and `Secret`.
|
||||||
- `get`: Retrieves credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
|
- `get`: Retrieves credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
|
||||||
- `erase`: Removes credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
|
- `erase`: Removes credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
|
||||||
|
- `list`: Lists stored credentials. There is no standard input payload.
|
||||||
|
|
||||||
This repository also includes libraries to implement new credentials programs in Go. Adding a new helper program is pretty easy. You can see how the OS X keychain helper works in the [osxkeychain](osxkeychain) directory.
|
This repository also includes libraries to implement new credentials programs in Go. Adding a new helper program is pretty easy. You can see how the OS X keychain helper works in the [osxkeychain](osxkeychain) directory.
|
||||||
|
|
||||||
|
|||||||
@@ -1,66 +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
|
|
||||||
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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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.
|
||||||
|
func Store(program ProgramFunc, creds *credentials.Credentials) error {
|
||||||
|
cmd := program("store")
|
||||||
|
|
||||||
|
buffer := new(bytes.Buffer)
|
||||||
|
if err := json.NewEncoder(buffer).Encode(creds); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.Input(buffer)
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
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 nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get executes an external program to get the credentials from a native store.
|
||||||
|
func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error) {
|
||||||
|
cmd := program("get")
|
||||||
|
cmd.Input(strings.NewReader(serverURL))
|
||||||
|
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
t := strings.TrimSpace(string(out))
|
||||||
|
|
||||||
|
if credentials.IsErrCredentialsNotFoundMessage(t) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &credentials.Credentials{
|
||||||
|
ServerURL: serverURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewDecoder(bytes.NewReader(out)).Decode(resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Erase executes a program to remove the server credentials from the native store.
|
||||||
|
func Erase(program ProgramFunc, serverURL string) error {
|
||||||
|
cmd := program("erase")
|
||||||
|
cmd.Input(strings.NewReader(serverURL))
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
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 nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// List executes a program to list server credentials in the native store.
|
||||||
|
func List(program ProgramFunc) (map[string]string, error) {
|
||||||
|
cmd := program("list")
|
||||||
|
cmd.Input(strings.NewReader("unused"))
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string]string
|
||||||
|
if err = json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
validServerAddress = "https://index.docker.io/v1"
|
||||||
|
validUsername = "linus"
|
||||||
|
validServerAddress2 = "https://example.com:5002"
|
||||||
|
invalidServerAddress = "https://foobar.example.com"
|
||||||
|
missingCredsAddress = "https://missing.docker.io/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errProgramExited = fmt.Errorf("exited 1")
|
||||||
|
|
||||||
|
// mockProgram simulates interactions between the docker client and a remote
|
||||||
|
// credentials helper.
|
||||||
|
// Unit tests inject this mocked command into the remote to control execution.
|
||||||
|
type mockProgram struct {
|
||||||
|
arg string
|
||||||
|
input io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output returns responses from the remote credentials helper.
|
||||||
|
// It mocks those responses based in the input in the mock.
|
||||||
|
func (m *mockProgram) Output() ([]byte, error) {
|
||||||
|
in, err := ioutil.ReadAll(m.input)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inS := string(in)
|
||||||
|
|
||||||
|
switch m.arg {
|
||||||
|
case "erase":
|
||||||
|
switch inS {
|
||||||
|
case validServerAddress:
|
||||||
|
return nil, nil
|
||||||
|
default:
|
||||||
|
return []byte("program failed"), errProgramExited
|
||||||
|
}
|
||||||
|
case "get":
|
||||||
|
switch inS {
|
||||||
|
case validServerAddress:
|
||||||
|
return []byte(`{"Username": "foo", "Secret": "bar"}`), nil
|
||||||
|
case validServerAddress2:
|
||||||
|
return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil
|
||||||
|
case missingCredsAddress:
|
||||||
|
return []byte(credentials.NewErrCredentialsNotFound().Error()), errProgramExited
|
||||||
|
case invalidServerAddress:
|
||||||
|
return []byte("program failed"), errProgramExited
|
||||||
|
case "":
|
||||||
|
return []byte(credentials.NewErrCredentialsMissingServerURL().Error()), errProgramExited
|
||||||
|
}
|
||||||
|
case "store":
|
||||||
|
var c credentials.Credentials
|
||||||
|
err := json.NewDecoder(strings.NewReader(inS)).Decode(&c)
|
||||||
|
if err != nil {
|
||||||
|
return []byte("error storing credentials"), errProgramExited
|
||||||
|
}
|
||||||
|
switch c.ServerURL {
|
||||||
|
case validServerAddress:
|
||||||
|
return nil, nil
|
||||||
|
case validServerAddress2:
|
||||||
|
return nil, nil
|
||||||
|
default:
|
||||||
|
return []byte("error storing credentials"), errProgramExited
|
||||||
|
}
|
||||||
|
case "list":
|
||||||
|
return []byte(fmt.Sprintf(`{"%s": "%s"}`, validServerAddress, validUsername)), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errProgramExited
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input sets the input to send to a remote credentials helper.
|
||||||
|
func (m *mockProgram) Input(in io.Reader) {
|
||||||
|
m.input = in
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockProgramFn(args ...string) Program {
|
||||||
|
return &mockProgram{
|
||||||
|
arg: args[0],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleStore() {
|
||||||
|
p := NewShellProgramFunc("docker-credential-secretservice")
|
||||||
|
|
||||||
|
c := &credentials.Credentials{
|
||||||
|
ServerURL: "https://example.com",
|
||||||
|
Username: "calavera",
|
||||||
|
Secret: "my super secret token",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Store(p, c); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStore(t *testing.T) {
|
||||||
|
valid := []credentials.Credentials{
|
||||||
|
{validServerAddress, "foo", "bar"},
|
||||||
|
{validServerAddress2, "<token>", "abcd1234"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range valid {
|
||||||
|
if err := Store(mockProgramFn, &v); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid := []credentials.Credentials{
|
||||||
|
{invalidServerAddress, "foo", "bar"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range invalid {
|
||||||
|
if err := Store(mockProgramFn, &v); err == nil {
|
||||||
|
t.Fatalf("Expected error for server %s, got nil", v.ServerURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleGet() {
|
||||||
|
p := NewShellProgramFunc("docker-credential-secretservice")
|
||||||
|
|
||||||
|
creds, err := Get(p, "https://example.com")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Got credentials for user `%s` in `%s`\n", creds.Username, creds.ServerURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet(t *testing.T) {
|
||||||
|
valid := []credentials.Credentials{
|
||||||
|
{validServerAddress, "foo", "bar"},
|
||||||
|
{validServerAddress2, "<token>", "abcd1234"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range valid {
|
||||||
|
c, err := Get(mockProgramFn, v.ServerURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Username != v.Username {
|
||||||
|
t.Fatalf("expected username `%s`, got %s", v.Username, c.Username)
|
||||||
|
}
|
||||||
|
if c.Secret != v.Secret {
|
||||||
|
t.Fatalf("expected secret `%s`, got %s", v.Secret, c.Secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
missingServerURLErr := credentials.NewErrCredentialsMissingServerURL()
|
||||||
|
|
||||||
|
invalid := []struct {
|
||||||
|
serverURL string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{missingCredsAddress, credentials.NewErrCredentialsNotFound().Error()},
|
||||||
|
{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 {
|
||||||
|
_, err := Get(mockProgramFn, v.serverURL)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error for server %s, got nil", v.serverURL)
|
||||||
|
}
|
||||||
|
if err.Error() != v.err {
|
||||||
|
t.Fatalf("Expected error `%s`, got `%v`", v.err, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleErase() {
|
||||||
|
p := NewShellProgramFunc("docker-credential-secretservice")
|
||||||
|
|
||||||
|
if err := Erase(p, "https://example.com"); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErase(t *testing.T) {
|
||||||
|
if err := Erase(mockProgramFn, validServerAddress); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := Erase(mockProgramFn, invalidServerAddress); err == nil {
|
||||||
|
t.Fatalf("Expected error for server %s, got nil", invalidServerAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
auths, err := List(mockProgramFn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if username, exists := auths[validServerAddress]; !exists || username != validUsername {
|
||||||
|
t.Fatalf("auths[%s] returned %s, %t; expected %s, %t", validServerAddress, username, exists, validUsername, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
exec "golang.org/x/sys/execabs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Program is an interface to execute external programs.
|
||||||
|
type Program interface {
|
||||||
|
Output() ([]byte, error)
|
||||||
|
Input(in io.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProgramFunc is a type of function that initializes programs based on arguments.
|
||||||
|
type ProgramFunc func(args ...string) Program
|
||||||
|
|
||||||
|
// NewShellProgramFunc creates programs that are executed in a Shell.
|
||||||
|
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 &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.
|
||||||
|
type Shell struct {
|
||||||
|
cmd *exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output returns responses from the remote credentials helper.
|
||||||
|
func (s *Shell) Output() ([]byte, error) {
|
||||||
|
return s.cmd.Output()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input sets the input to send to a remote credentials helper.
|
||||||
|
func (s *Shell) Input(in io.Reader) {
|
||||||
|
s.cmd.Stdin = in
|
||||||
|
}
|
||||||
+96
-20
@@ -10,36 +10,80 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type credentialsGetResponse struct {
|
// Credentials holds the information shared between docker and the credentials store.
|
||||||
Username string
|
type Credentials struct {
|
||||||
Secret string
|
ServerURL string
|
||||||
|
Username 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.
|
||||||
|
// It uses os.Args[1] as the key for the action.
|
||||||
|
// It uses os.Stdin as input and os.Stdout as output.
|
||||||
|
// This function terminates the program with os.Exit(1) if there is an error.
|
||||||
func Serve(helper Helper) {
|
func Serve(helper Helper) {
|
||||||
if err := handleCommand(helper); err != nil {
|
var err error
|
||||||
|
if len(os.Args) != 2 {
|
||||||
|
err = fmt.Errorf("Usage: %s <store|get|erase|list|version>", os.Args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
err = HandleCommand(helper, os.Args[1], os.Stdin, os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stdout, "%v\n", err)
|
fmt.Fprintf(os.Stdout, "%v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleCommand(helper Helper) error {
|
// HandleCommand uses a helper and a key to run a credential action.
|
||||||
if len(os.Args) != 2 {
|
func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error {
|
||||||
return fmt.Errorf("Usage: %s <store|get|erase>", os.Args[0])
|
switch key {
|
||||||
}
|
|
||||||
|
|
||||||
switch os.Args[1] {
|
|
||||||
case "store":
|
case "store":
|
||||||
return store(helper, os.Stdin)
|
return Store(helper, in)
|
||||||
case "get":
|
case "get":
|
||||||
return get(helper, os.Stdin, os.Stdout)
|
return Get(helper, in, out)
|
||||||
case "erase":
|
case "erase":
|
||||||
return erase(helper, os.Stdin)
|
return Erase(helper, in)
|
||||||
|
case "list":
|
||||||
|
return List(helper, out)
|
||||||
|
case "version":
|
||||||
|
return PrintVersion(out)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("Usage: %s <store|get|erase>", os.Args[0])
|
return fmt.Errorf("Unknown credential action `%s`", key)
|
||||||
}
|
}
|
||||||
|
|
||||||
func store(helper Helper, reader io.Reader) error {
|
// Store uses a helper and an input reader to save credentials.
|
||||||
|
// The reader must contain the JSON serialization of a Credentials struct.
|
||||||
|
func Store(helper Helper, reader io.Reader) error {
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
@@ -56,10 +100,17 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func get(helper Helper, reader io.Reader, writer io.Writer) error {
|
// Get retrieves the credentials for a given server url.
|
||||||
|
// The reader must contain the server URL to search.
|
||||||
|
// The writer is used to write the JSON serialization of the credentials.
|
||||||
|
func Get(helper Helper, reader io.Reader, writer io.Writer) error {
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
@@ -72,15 +123,19 @@ 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 {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp := credentialsGetResponse{
|
resp := Credentials{
|
||||||
Username: username,
|
ServerURL: serverURL,
|
||||||
Secret: secret,
|
Username: username,
|
||||||
|
Secret: secret,
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.Reset()
|
buffer.Reset()
|
||||||
@@ -92,7 +147,9 @@ func get(helper Helper, reader io.Reader, writer io.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func erase(helper Helper, reader io.Reader) error {
|
// Erase removes credentials from the store.
|
||||||
|
// The reader must contain the server URL to remove.
|
||||||
|
func Erase(helper Helper, reader io.Reader) error {
|
||||||
scanner := bufio.NewScanner(reader)
|
scanner := bufio.NewScanner(reader)
|
||||||
|
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
@@ -105,6 +162,25 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//List returns all the serverURLs of keys in
|
||||||
|
//the OS store as a list of strings
|
||||||
|
func List(helper Helper, writer io.Writer) error {
|
||||||
|
accts, err := helper.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.NewEncoder(writer).Encode(accts)
|
||||||
|
}
|
||||||
|
|
||||||
|
//PrintVersion outputs the current version.
|
||||||
|
func PrintVersion(writer io.Writer) error {
|
||||||
|
fmt.Fprintln(writer, Version)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ func (m *memoryStore) Get(serverURL string) (string, string, error) {
|
|||||||
return c.Username, c.Secret, nil
|
return c.Username, c.Secret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *memoryStore) List() (map[string]string, error) {
|
||||||
|
//Simply a placeholder to let memoryStore be a valid implementation of Helper interface
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestStore(t *testing.T) {
|
func TestStore(t *testing.T) {
|
||||||
serverURL := "https://index.docker.io/v1/"
|
serverURL := "https://index.docker.io/v1/"
|
||||||
creds := &Credentials{
|
creds := &Credentials{
|
||||||
@@ -50,7 +55,7 @@ func TestStore(t *testing.T) {
|
|||||||
in := bytes.NewReader(b)
|
in := bytes.NewReader(b)
|
||||||
|
|
||||||
h := newMemoryStore()
|
h := newMemoryStore()
|
||||||
if err := store(h, in); err != nil {
|
if err := Store(h, in); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,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{
|
||||||
@@ -82,13 +127,13 @@ func TestGet(t *testing.T) {
|
|||||||
in := bytes.NewReader(b)
|
in := bytes.NewReader(b)
|
||||||
|
|
||||||
h := newMemoryStore()
|
h := newMemoryStore()
|
||||||
if err := store(h, in); err != nil {
|
if err := Store(h, in); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := strings.NewReader(serverURL)
|
buf := strings.NewReader(serverURL)
|
||||||
w := new(bytes.Buffer)
|
w := new(bytes.Buffer)
|
||||||
if err := get(h, buf, w); err != nil {
|
if err := Get(h, buf, w); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +141,7 @@ func TestGet(t *testing.T) {
|
|||||||
t.Fatalf("expected output in the writer, got %d", w.Len())
|
t.Fatalf("expected output in the writer, got %d", w.Len())
|
||||||
}
|
}
|
||||||
|
|
||||||
var c credentialsGetResponse
|
var c Credentials
|
||||||
if err := json.NewDecoder(w).Decode(&c); err != nil {
|
if err := json.NewDecoder(w).Decode(&c); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -110,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{
|
||||||
@@ -124,17 +195,55 @@ func TestErase(t *testing.T) {
|
|||||||
in := bytes.NewReader(b)
|
in := bytes.NewReader(b)
|
||||||
|
|
||||||
h := newMemoryStore()
|
h := newMemoryStore()
|
||||||
if err := store(h, in); err != nil {
|
if err := Store(h, in); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := strings.NewReader(serverURL)
|
buf := strings.NewReader(serverURL)
|
||||||
if err := erase(h, buf); err != nil {
|
if err := Erase(h, buf); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w := new(bytes.Buffer)
|
w := new(bytes.Buffer)
|
||||||
if err := get(h, buf, w); err == nil {
|
if err := Get(h, buf, w); err == nil {
|
||||||
t.Fatal("expected error getting missing creds, got empty")
|
t.Fatal("expected error getting missing creds, got empty")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
//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
|
||||||
|
out := new(bytes.Buffer)
|
||||||
|
h := newMemoryStore()
|
||||||
|
if err := List(h, out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
//testing that there is an output
|
||||||
|
if out.Len() == 0 {
|
||||||
|
t.Fatalf("expected output in the writer, got %d", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package credentials
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ErrCredentialsNotFound standardizes the not found error, so every helper returns
|
||||||
|
// 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
|
||||||
|
// raised when credentials are not in the store.
|
||||||
|
type errCredentialsNotFound struct{}
|
||||||
|
|
||||||
|
// Error returns the standard error message
|
||||||
|
// for when the credentials are not in the store.
|
||||||
|
func (errCredentialsNotFound) Error() string {
|
||||||
|
return errCredentialsNotFoundMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewErrCredentialsNotFound creates a new error
|
||||||
|
// for when the credentials are not in the store.
|
||||||
|
func NewErrCredentialsNotFound() error {
|
||||||
|
return errCredentialsNotFound{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrCredentialsNotFound returns true if the error
|
||||||
|
// was caused by not having a set of credentials in a store.
|
||||||
|
func IsErrCredentialsNotFound(err error) bool {
|
||||||
|
_, ok := err.(errCredentialsNotFound)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrCredentialsNotFoundMessage returns true if the error
|
||||||
|
// was caused by not having a set of credentials in a store.
|
||||||
|
//
|
||||||
|
// This function helps to check messages returned by an
|
||||||
|
// external program via its standard output.
|
||||||
|
func IsErrCredentialsNotFoundMessage(err string) bool {
|
||||||
|
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
|
||||||
|
}
|
||||||
+6
-13
@@ -1,21 +1,14 @@
|
|||||||
package credentials
|
package credentials
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
// Credentials holds the information shared between docker and the credentials store.
|
|
||||||
type Credentials struct {
|
|
||||||
ServerURL string
|
|
||||||
Username string
|
|
||||||
Secret string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper is the interface a credentials store helper must implement.
|
// Helper is the interface a credentials store helper must implement.
|
||||||
type Helper interface {
|
type Helper interface {
|
||||||
|
// Add appends credentials to the store.
|
||||||
Add(*Credentials) error
|
Add(*Credentials) error
|
||||||
|
// Delete removes credentials from the store.
|
||||||
Delete(serverURL string) error
|
Delete(serverURL string) error
|
||||||
|
// Get retrieves credentials from the store.
|
||||||
|
// It returns username and secret as strings.
|
||||||
Get(serverURL string) (string, string, error)
|
Get(serverURL string) (string, string, error)
|
||||||
|
// List returns the stored serverURLs and their associated usernames.
|
||||||
|
List() (map[string]string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrCredentialsNotFound standarizes the not found error, so every helper returns
|
|
||||||
// the same message and docker can handle it properly.
|
|
||||||
var ErrCredentialsNotFound = errors.New("credentials not found in native keychain")
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package credentials
|
||||||
|
|
||||||
|
// Version holds a string describing the current version
|
||||||
|
const Version = "0.6.4"
|
||||||
@@ -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
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
9
|
||||||
@@ -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
|
||||||
Executable
+17
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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=
|
||||||
@@ -6,5 +6,5 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
credentials.Serve(osxkeychain.New())
|
credentials.Serve(osxkeychain.Osxkeychain{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
#include "osxkeychain_darwin.h"
|
#include "osxkeychain_darwin.h"
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include <Foundation/NSValue.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
char *get_error(OSStatus status) {
|
char *get_error(OSStatus status) {
|
||||||
char *buf = malloc(128);
|
char *buf = malloc(128);
|
||||||
@@ -10,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,
|
||||||
@@ -21,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,3 +118,110 @@ char *keychain_delete(struct Server *server) {
|
|||||||
}
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char * CFStringToCharArr(CFStringRef aString) {
|
||||||
|
if (aString == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
CFIndex length = CFStringGetLength(aString);
|
||||||
|
CFIndex maxSize =
|
||||||
|
CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
|
||||||
|
char *buffer = (char *)malloc(maxSize);
|
||||||
|
if (CFStringGetCString(aString, buffer, maxSize,
|
||||||
|
kCFStringEncodingUTF8)) {
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword);
|
||||||
|
CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue);
|
||||||
|
CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll);
|
||||||
|
CFDictionaryAddValue(query, kSecAttrLabel, credsLabelCF);
|
||||||
|
//Use this query dictionary
|
||||||
|
CFTypeRef result= NULL;
|
||||||
|
OSStatus status = SecItemCopyMatching(
|
||||||
|
query,
|
||||||
|
&result);
|
||||||
|
|
||||||
|
CFRelease(credsLabelCF);
|
||||||
|
|
||||||
|
//Ran a search and store the results in result
|
||||||
|
if (status) {
|
||||||
|
return get_error(status);
|
||||||
|
}
|
||||||
|
CFIndex numKeys = CFArrayGetCount(result);
|
||||||
|
*paths = (char **) malloc((int)sizeof(char *)*numKeys);
|
||||||
|
*accts = (char **) malloc((int)sizeof(char *)*numKeys);
|
||||||
|
//result is of type CFArray
|
||||||
|
for(CFIndex i=0; i<numKeys; i++) {
|
||||||
|
CFDictionaryRef currKey = CFArrayGetValueAtIndex(result,i);
|
||||||
|
|
||||||
|
CFStringRef protocolTmp = CFDictionaryGetValue(currKey, CFSTR("ptcl"));
|
||||||
|
if (protocolTmp != NULL) {
|
||||||
|
CFStringRef protocolStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@"), protocolTmp);
|
||||||
|
if (CFStringCompare(protocolStr, CFSTR("htps"), 0) == kCFCompareEqualTo) {
|
||||||
|
protocolTmp = CFSTR("https://");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
protocolTmp = CFSTR("http://");
|
||||||
|
}
|
||||||
|
CFRelease(protocolStr);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
char * path = "0";
|
||||||
|
char * acct = "0";
|
||||||
|
(*paths)[i] = (char *) malloc(sizeof(char)*(strlen(path)));
|
||||||
|
memcpy((*paths)[i], path, sizeof(char)*(strlen(path)));
|
||||||
|
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)));
|
||||||
|
memcpy((*accts)[i], acct, sizeof(char)*(strlen(acct)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CFMutableStringRef str = CFStringCreateMutableCopy(NULL, 0, protocolTmp);
|
||||||
|
CFStringRef serverTmp = CFDictionaryGetValue(currKey, CFSTR("srvr"));
|
||||||
|
if (serverTmp != NULL) {
|
||||||
|
CFStringAppend(str, serverTmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
CFStringRef pathTmp = CFDictionaryGetValue(currKey, CFSTR("path"));
|
||||||
|
if (pathTmp != NULL) {
|
||||||
|
CFStringAppend(str, pathTmp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NSNumber * portTmp = CFDictionaryGetValue(currKey, CFSTR("port"));
|
||||||
|
if (portTmp != NULL && portTmp.integerValue != 0) {
|
||||||
|
CFStringRef portStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@"), portTmp);
|
||||||
|
CFStringAppend(str, CFSTR(":"));
|
||||||
|
CFStringAppend(str, portStr);
|
||||||
|
CFRelease(portStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
CFStringRef acctTmp = CFDictionaryGetValue(currKey, CFSTR("acct"));
|
||||||
|
if (acctTmp == NULL) {
|
||||||
|
acctTmp = CFSTR("account not defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
char * path = CFStringToCharArr(str);
|
||||||
|
char * acct = CFStringToCharArr(acctTmp);
|
||||||
|
|
||||||
|
//We now have all we need, username and servername. Now export this to .go
|
||||||
|
(*paths)[i] = (char *) malloc(sizeof(char)*(strlen(path)+1));
|
||||||
|
memcpy((*paths)[i], path, sizeof(char)*(strlen(path)+1));
|
||||||
|
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)+1));
|
||||||
|
memcpy((*accts)[i], acct, sizeof(char)*(strlen(acct)+1));
|
||||||
|
|
||||||
|
CFRelease(str);
|
||||||
|
}
|
||||||
|
*list_l = (int)numKeys;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void freeListData(char *** data, unsigned int length) {
|
||||||
|
for(int i=0; i<length; i++) {
|
||||||
|
free((*data)[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package osxkeychain
|
package osxkeychain
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#cgo CFLAGS: -x objective-c
|
#cgo CFLAGS: -x objective-c -mmacosx-version-min=10.11
|
||||||
#cgo LDFLAGS: -framework Security -framework Foundation
|
#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,39 +10,45 @@ 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."
|
||||||
|
|
||||||
type osxkeychain struct{}
|
// 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."
|
||||||
|
|
||||||
// New creates a new osxkeychain.
|
// ErrInteractionNotAllowed is returned if keychain password prompt can not be shown.
|
||||||
func New() credentials.Helper {
|
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`)
|
||||||
return osxkeychain{}
|
|
||||||
}
|
// Osxkeychain handles secrets using the OS X Keychain as store.
|
||||||
|
type Osxkeychain struct{}
|
||||||
|
|
||||||
// Add adds new credentials to the keychain.
|
// Add adds new credentials to the keychain.
|
||||||
func (h osxkeychain) Add(creds *credentials.Credentials) error {
|
func (h Osxkeychain) Add(creds *credentials.Credentials) error {
|
||||||
|
h.Delete(creds.ServerURL)
|
||||||
|
|
||||||
s, err := splitServer(creds.ServerURL)
|
s, err := splitServer(creds.ServerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
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))
|
||||||
@@ -52,7 +58,7 @@ func (h osxkeychain) Add(creds *credentials.Credentials) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes credentials from the keychain.
|
// Delete removes credentials from the keychain.
|
||||||
func (h osxkeychain) Delete(serverURL string) error {
|
func (h Osxkeychain) Delete(serverURL string) error {
|
||||||
s, err := splitServer(serverURL)
|
s, err := splitServer(serverURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -69,7 +75,7 @@ func (h osxkeychain) Delete(serverURL string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the username and secret to use for a given registry server URL.
|
// Get returns the username and secret to use for a given registry server URL.
|
||||||
func (h osxkeychain) Get(serverURL string) (string, string, error) {
|
func (h Osxkeychain) Get(serverURL string) (string, string, error) {
|
||||||
s, err := splitServer(serverURL)
|
s, err := splitServer(serverURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
@@ -87,9 +93,11 @@ func (h osxkeychain) Get(serverURL string) (string, string, error) {
|
|||||||
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 {
|
if goMsg == errCredentialsNotFound {
|
||||||
return "", "", credentials.ErrCredentialsNotFound
|
return "", "", credentials.NewErrCredentialsNotFound()
|
||||||
|
}
|
||||||
|
if goMsg == errInteractionNotAllowed {
|
||||||
|
return "", "", ErrInteractionNotAllowed
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", "", errors.New(goMsg)
|
return "", "", errors.New(goMsg)
|
||||||
@@ -100,31 +108,69 @@ func (h osxkeychain) Get(serverURL string) (string, string, error) {
|
|||||||
return user, pass, nil
|
return user, pass, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns the stored URLs and corresponding usernames.
|
||||||
|
func (h Osxkeychain) List() (map[string]string, error) {
|
||||||
|
credsLabelC := C.CString(credentials.CredsLabel)
|
||||||
|
defer C.free(unsafe.Pointer(credsLabelC))
|
||||||
|
|
||||||
|
var pathsC **C.char
|
||||||
|
defer C.free(unsafe.Pointer(pathsC))
|
||||||
|
var acctsC **C.char
|
||||||
|
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
|
||||||
|
}
|
||||||
|
if goMsg == errInteractionNotAllowed {
|
||||||
|
return nil, ErrInteractionNotAllowed
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.New(goMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var listLen int
|
||||||
|
listLen = int(listLenC)
|
||||||
|
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
|
||||||
|
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
|
||||||
|
//taking the array of c strings into go while ignoring all the stuff irrelevant to credentials-helper
|
||||||
|
resp := make(map[string]string)
|
||||||
|
for i := 0; i < listLen; i++ {
|
||||||
|
if C.GoString(pathTmp[i]) == "0" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i])
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -7,6 +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 *credsLabel, char *** data, char *** accts, unsigned int *list_l);
|
||||||
|
void freeListData(char *** data, unsigned int length);
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package osxkeychain
|
package osxkeychain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/docker-credential-helpers/credentials"
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
@@ -12,8 +13,12 @@ func TestOSXKeychainHelper(t *testing.T) {
|
|||||||
Username: "foobar",
|
Username: "foobar",
|
||||||
Secret: "foobarbaz",
|
Secret: "foobarbaz",
|
||||||
}
|
}
|
||||||
|
creds1 := &credentials.Credentials{
|
||||||
helper := New()
|
ServerURL: "https://foobar.docker.io:2376/v2",
|
||||||
|
Username: "foobarbaz",
|
||||||
|
Secret: "foobar",
|
||||||
|
}
|
||||||
|
helper := Osxkeychain{}
|
||||||
if err := helper.Add(creds); err != nil {
|
if err := helper.Add(creds); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -31,15 +36,178 @@ func TestOSXKeychainHelper(t *testing.T) {
|
|||||||
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auths, err := helper.List()
|
||||||
|
if err != nil || len(auths) == 0 {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.Add(creds1)
|
||||||
|
defer helper.Delete(creds1.ServerURL)
|
||||||
|
newauths, err := helper.List()
|
||||||
|
if len(newauths)-len(auths) != 1 {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Error: len(newauths): %d, len(auths): %d", len(newauths), len(auths))
|
||||||
|
}
|
||||||
|
t.Fatalf("Error: len(newauths): %d, len(auths): %d\n Error= %v", len(newauths), len(auths), err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMissingCredentials(t *testing.T) {
|
// TestOSXKeychainHelperRetrieveAliases verifies that secrets can be accessed
|
||||||
helper := New()
|
// through variations on the URL
|
||||||
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
func TestOSXKeychainHelperRetrieveAliases(t *testing.T) {
|
||||||
if err != credentials.ErrCredentialsNotFound {
|
tests := []struct {
|
||||||
t.Fatalf("exptected ErrCredentialsNotFound, got %v", err)
|
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) {
|
||||||
|
helper := Osxkeychain{}
|
||||||
|
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
||||||
|
if !credentials.IsErrCredentialsNotFound(err) {
|
||||||
|
t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
}
|
||||||
@@ -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?")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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(":"):]
|
||||||
|
}
|
||||||
@@ -6,5 +6,5 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
credentials.Serve(secretservice.New())
|
credentials.Serve(secretservice.Secretservice{})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include "secretservice_linux.h"
|
#include "secretservice_linux.h"
|
||||||
|
|
||||||
const SecretSchema *docker_get_schema(void)
|
const SecretSchema *docker_get_schema(void)
|
||||||
@@ -6,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 },
|
||||||
@@ -15,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",
|
||||||
@@ -39,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;
|
||||||
@@ -47,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);
|
||||||
@@ -70,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);
|
||||||
@@ -84,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);
|
||||||
}
|
}
|
||||||
@@ -96,3 +99,63 @@ GError *get(char *server, char **username, char **secret) {
|
|||||||
}
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GError *list(char *ref_label, char *** paths, char *** accts, unsigned int *list_l) {
|
||||||
|
GList *items;
|
||||||
|
GError *err = NULL;
|
||||||
|
SecretService *service;
|
||||||
|
SecretSearchFlags flags = SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK;
|
||||||
|
GHashTable *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);
|
||||||
|
if (err != NULL) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
items = secret_service_search_sync(service, NULL, attributes, flags, NULL, &err);
|
||||||
|
int numKeys = g_list_length(items);
|
||||||
|
if (err != NULL) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
// we will now put it in our two lists to return it to go
|
||||||
|
GList *current;
|
||||||
|
int listNumber = 0;
|
||||||
|
for(current = items; current!=NULL; current = current->next) {
|
||||||
|
char *pathTmp = secret_item_get_label(current->data);
|
||||||
|
// you cannot have a key without a label in the gnome keyring
|
||||||
|
char *acctTmp = get_attribute("username",current->data);
|
||||||
|
if (acctTmp==NULL) {
|
||||||
|
acctTmp = "account not defined";
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp_paths[listNumber] = (char *) calloc(1, sizeof(char)*(strlen(pathTmp)+1));
|
||||||
|
tmp_accts[listNumber] = (char *) calloc(1, sizeof(char)*(strlen(acctTmp)+1));
|
||||||
|
|
||||||
|
memcpy(tmp_paths[listNumber], pathTmp, sizeof(char)*(strlen(pathTmp)+1));
|
||||||
|
memcpy(tmp_accts[listNumber], acctTmp, sizeof(char)*(strlen(acctTmp)+1));
|
||||||
|
|
||||||
|
listNumber = listNumber + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*paths = (char **) realloc(tmp_paths, (int)sizeof(char *)*listNumber);
|
||||||
|
*accts = (char **) realloc(tmp_accts, (int)sizeof(char *)*listNumber);
|
||||||
|
|
||||||
|
*list_l = listNumber;
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void freeListData(char *** data, unsigned int length) {
|
||||||
|
int i;
|
||||||
|
for(i=0; i<length; i++) {
|
||||||
|
free((*data)[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,18 +14,16 @@ import (
|
|||||||
"github.com/docker/docker-credential-helpers/credentials"
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
type secretservice struct{}
|
// Secretservice handles secrets using Linux secret-service as a store.
|
||||||
|
type Secretservice struct{}
|
||||||
// New creates a new secretservice.
|
|
||||||
func New() credentials.Helper {
|
|
||||||
return secretservice{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add adds new credentials to the keychain.
|
// Add adds new credentials to the keychain.
|
||||||
func (h secretservice) Add(creds *credentials.Credentials) error {
|
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)
|
||||||
@@ -33,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))
|
||||||
@@ -41,8 +39,8 @@ func (h secretservice) Add(creds *credentials.Credentials) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes credentials from the keychain.
|
// Delete removes credentials from the store.
|
||||||
func (h secretservice) Delete(serverURL string) error {
|
func (h Secretservice) Delete(serverURL string) error {
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
return errors.New("missing server url")
|
return errors.New("missing server url")
|
||||||
}
|
}
|
||||||
@@ -58,7 +56,7 @@ func (h secretservice) Delete(serverURL string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get returns the username and secret to use for a given registry server URL.
|
// Get returns the username and secret to use for a given registry server URL.
|
||||||
func (h secretservice) Get(serverURL string) (string, string, error) {
|
func (h Secretservice) Get(serverURL string) (string, string, error) {
|
||||||
if serverURL == "" {
|
if serverURL == "" {
|
||||||
return "", "", errors.New("missing server url")
|
return "", "", errors.New("missing server url")
|
||||||
}
|
}
|
||||||
@@ -78,7 +76,43 @@ func (h secretservice) Get(serverURL string) (string, string, error) {
|
|||||||
user := C.GoString(username)
|
user := C.GoString(username)
|
||||||
pass := C.GoString(secret)
|
pass := C.GoString(secret)
|
||||||
if pass == "" {
|
if pass == "" {
|
||||||
return "", "", credentials.ErrCredentialsNotFound
|
return "", "", credentials.NewErrCredentialsNotFound()
|
||||||
}
|
}
|
||||||
return user, pass, nil
|
return user, pass, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns the stored URLs and corresponding usernames for a given credentials label
|
||||||
|
func (h Secretservice) List() (map[string]string, error) {
|
||||||
|
credsLabelC := C.CString(credentials.CredsLabel)
|
||||||
|
defer C.free(unsafe.Pointer(credsLabelC))
|
||||||
|
|
||||||
|
var pathsC **C.char
|
||||||
|
defer C.free(unsafe.Pointer(pathsC))
|
||||||
|
var acctsC **C.char
|
||||||
|
defer C.free(unsafe.Pointer(acctsC))
|
||||||
|
var listLenC C.uint
|
||||||
|
err := C.list(credsLabelC, &pathsC, &acctsC, &listLenC)
|
||||||
|
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)
|
||||||
|
|
||||||
|
listLen := int(listLenC)
|
||||||
|
if listLen == 0 {
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
// 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++ {
|
||||||
|
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +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 *label, char *** paths, char *** accts, unsigned int *list_l);
|
||||||
|
void freeListData(char *** data, unsigned int length);
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -15,11 +16,37 @@ func TestSecretServiceHelper(t *testing.T) {
|
|||||||
Secret: "foobarbaz",
|
Secret: "foobarbaz",
|
||||||
}
|
}
|
||||||
|
|
||||||
helper := New()
|
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,17 +60,31 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We should have one less credential than before deleting
|
||||||
|
newAuths, err = helper.List()
|
||||||
|
if err != nil || (len(oldAuths)-len(newAuths) != 1) {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMissingCredentials(t *testing.T) {
|
func TestMissingCredentials(t *testing.T) {
|
||||||
t.Skip("test requires gnome-keyring but travis CI doesn't have it")
|
t.Skip("test requires gnome-keyring but travis CI doesn't have it")
|
||||||
|
|
||||||
helper := New()
|
helper := Secretservice{}
|
||||||
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
||||||
if err != credentials.ErrCredentialsNotFound {
|
if !credentials.IsErrCredentialsNotFound(err) {
|
||||||
t.Fatalf("exptected ErrCredentialsNotFound, got %v", err)
|
t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
*.go text eol=lf
|
||||||
+23
@@ -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
@@ -0,0 +1,98 @@
|
|||||||
|
wincred
|
||||||
|
=======
|
||||||
|
|
||||||
|
Go wrapper around the Windows Credential Manager API functions.
|
||||||
|
|
||||||
|

|
||||||
|
[](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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
+50
-23
@@ -1,7 +1,8 @@
|
|||||||
|
// +build windows
|
||||||
|
|
||||||
package wincred
|
package wincred
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"C"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"reflect"
|
"reflect"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -10,34 +11,59 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create a Go string using a pointer to a zero-terminated UTF 16 encoded string.
|
// uf16PtrToString creates a Go string from a pointer to a UTF16 encoded zero-terminated string.
|
||||||
// See github.com/AllenDang/w32
|
// Such pointers are returned from the Windows API calls.
|
||||||
|
// The function creates a copy of the string.
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// utf16FromString creates a UTF16 char array from a string.
|
||||||
|
func utf16FromString(str string) []uint16 {
|
||||||
|
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{}
|
||||||
|
}
|
||||||
|
rv := make([]byte, len)
|
||||||
|
copy(rv, *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{
|
||||||
|
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 cred == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
result = new(Credential)
|
result = new(Credential)
|
||||||
result.Comment = utf16PtrToString(cred.Comment)
|
result.Comment = utf16PtrToString(cred.Comment)
|
||||||
result.TargetName = utf16PtrToString(cred.TargetName)
|
result.TargetName = utf16PtrToString(cred.TargetName)
|
||||||
@@ -45,27 +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 = C.GoBytes(unsafe.Pointer(cred.CredentialBlob), C.int(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 = C.GoBytes(unsafe.Pointer(attr.Value), C.int(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) {
|
||||||
result = new(nativeCREDENTIAL)
|
if cred == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
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)
|
||||||
@@ -79,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
@@ -0,0 +1,11 @@
|
|||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package wincred
|
||||||
|
|
||||||
|
func utf16ToByte(...interface{}) []byte {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func utf16FromString(...interface{}) []uint16 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
module github.com/danieljoos/wincred
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.5.1
|
||||||
+12
@@ -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=
|
||||||
-99
@@ -1,99 +0,0 @@
|
|||||||
package wincred
|
|
||||||
|
|
||||||
import (
|
|
||||||
"syscall"
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
modadvapi32 = syscall.NewLazyDLL("advapi32.dll")
|
|
||||||
|
|
||||||
procCredRead = modadvapi32.NewProc("CredReadW")
|
|
||||||
procCredWrite = modadvapi32.NewProc("CredWriteW")
|
|
||||||
procCredDelete = modadvapi32.NewProc("CredDeleteW")
|
|
||||||
procCredFree = modadvapi32.NewProc("CredFree")
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
+143
@@ -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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
+61
-19
@@ -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,20 +70,42 @@ 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 retrieves all credentials of the Credentials store.
|
||||||
|
func List() ([]*Credential, error) {
|
||||||
|
creds, err := sysCredEnumerate("", true)
|
||||||
|
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
|
||||||
|
creds = []*Credential{}
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return creds, err
|
||||||
}
|
}
|
||||||
|
|||||||
+3
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
Vendored
+4
@@ -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
|
||||||
@@ -6,5 +6,5 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
credentials.Serve(wincred.New())
|
credentials.Serve(wincred.Wincred{})
|
||||||
}
|
}
|
||||||
|
|||||||
+120
-13
@@ -1,28 +1,32 @@
|
|||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
type wincred struct{}
|
// Wincred handles secrets using the Windows credential service.
|
||||||
|
type Wincred struct{}
|
||||||
// New creates a new wincred.
|
|
||||||
func New() credentials.Helper {
|
|
||||||
return wincred{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete removes credentials from the windows credentials manager.
|
// Delete removes credentials from the windows credentials manager.
|
||||||
func (h wincred) Delete(serverURL string) error {
|
func (h Wincred) Delete(serverURL string) error {
|
||||||
g, err := winc.GetGenericCredential(serverURL)
|
g, err := winc.GetGenericCredential(serverURL)
|
||||||
if g == nil {
|
if g == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -34,10 +38,113 @@ 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 g == nil {
|
if err != nil {
|
||||||
return "", "", credentials.ErrCredentialsNotFound
|
return "", "", err
|
||||||
|
} else if target == "" {
|
||||||
|
return "", "", credentials.NewErrCredentialsNotFound()
|
||||||
}
|
}
|
||||||
return g.UserName, string(g.CredentialBlob), nil
|
|
||||||
|
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 {
|
||||||
|
|
||||||
|
return g.UserName, string(g.CredentialBlob), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := make(map[string]string)
|
||||||
|
for i := range creds {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -12,8 +14,38 @@ func TestWinCredHelper(t *testing.T) {
|
|||||||
Username: "foobar",
|
Username: "foobar",
|
||||||
Secret: "foobarbaz",
|
Secret: "foobarbaz",
|
||||||
}
|
}
|
||||||
|
creds1 := &credentials.Credentials{
|
||||||
|
ServerURL: "https://foobar.docker.io:2376/v2",
|
||||||
|
Username: "foobarbaz",
|
||||||
|
Secret: "foobar",
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
helper := New()
|
|
||||||
if err := helper.Add(creds); err != nil {
|
if err := helper.Add(creds); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -31,15 +63,182 @@ func TestWinCredHelper(t *testing.T) {
|
|||||||
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auths, err := helper.List()
|
||||||
|
if err != nil || len(auths)-len(oldauths) != 1 {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.Add(creds1)
|
||||||
|
defer helper.Delete(creds1.ServerURL)
|
||||||
|
newauths, err := helper.List()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newauths)-len(auths) != 1 {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Error: len(newauths): %d, len(auths): %d", len(newauths), len(auths))
|
||||||
|
}
|
||||||
|
t.Fatalf("Error: len(newauths): %d, len(auths): %d\n Error= %v", len(newauths), len(auths), err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMissingCredentials(t *testing.T) {
|
// TestWinCredHelperRetrieveAliases verifies that secrets can be accessed
|
||||||
helper := New()
|
// through variations on the URL
|
||||||
_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")
|
func TestWinCredHelperRetrieveAliases(t *testing.T) {
|
||||||
if err != credentials.ErrCredentialsNotFound {
|
tests := []struct {
|
||||||
t.Fatalf("exptected ErrCredentialsNotFound, got %v", err)
|
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")
|
||||||
|
if !credentials.IsErrCredentialsNotFound(err) {
|
||||||
|
t.Fatalf("expected ErrCredentialsNotFound, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user