@@ -14,42 +14,73 @@ import (
1414 "os/exec"
1515 "path"
1616 "strings"
17+ "sync"
1718
1819 "github.com/docker/docker-credential-helpers/credentials"
1920)
2021
2122const PASS_FOLDER = "docker-credential-helpers"
2223
23- var (
24- PassInitialized bool
25- )
24+ // Pass handles secrets using Linux secret-service as a store.
25+ type Pass struct {}
26+
27+ // Ideally these would be stored as members of Pass, but since all of Pass's
28+ // methods have value receivers, not pointer receivers, and changing that is
29+ // backwards incompatible, we assume that all Pass instances share the same configuration
30+
31+ // initializationMutex is held while initializing so that only one 'pass'
32+ // round-tripping is done to check pass is functioning.
33+ var initializationMutex sync.Mutex
34+ var passInitialized bool
35+
36+ // CheckInitialized checks whether the password helper can be used. It
37+ // internally caches and so may be safely called multiple times with no impact
38+ // on performance, though the first call may take longer.
39+ func (p Pass ) CheckInitialized () bool {
40+ return p .checkInitialized () == nil
41+ }
2642
27- func init () {
43+ func (p Pass ) checkInitialized () error {
44+ initializationMutex .Lock ()
45+ defer initializationMutex .Unlock ()
46+ if passInitialized {
47+ return nil
48+ }
2849 // In principle, we could just run `pass init`. However, pass has a bug
2950 // where if gpg fails, it doesn't always exit 1. Additionally, pass
3051 // uses gpg2, but gpg is the default, which may be confusing. So let's
3152 // just explictily check that pass actually can store and retreive a
3253 // password.
3354 password := "pass is initialized"
34- name := path .Join (PASS_FOLDER , "docker-pass-initialized-check" )
55+ name := path .Join (getPassDir () , "docker-pass-initialized-check" )
3556
36- _ , err := runPass (password , "insert" , "-f" , "-m" , name )
57+ _ , err := p . runPassHelper (password , "insert" , "-f" , "-m" , name )
3758 if err != nil {
38- return
59+ return fmt . Errorf ( "error initializing pass: %v" , err )
3960 }
4061
41- stored , err := runPass ("" , "show" , name )
42- PassInitialized = err == nil && stored == password
62+ stored , err := p .runPassHelper ("" , "show" , name )
63+ if err != nil {
64+ return fmt .Errorf ("error fetching password during initialization: %v" , err )
65+ }
66+ if stored != password {
67+ return fmt .Errorf ("error round-tripping password during initialization: %q != %q" , password , stored )
68+ }
69+ passInitialized = true
70+ return nil
71+ }
4372
44- if PassInitialized {
45- runPass ("" , "rm" , "-rf" , name )
73+ func (p Pass ) runPass (stdinContent string , args ... string ) (string , error ) {
74+ if err := p .checkInitialized (); err != nil {
75+ return "" , err
4676 }
77+ return p .runPassHelper (stdinContent , args ... )
4778}
4879
49- func runPass ( stdin string , args ... string ) (string , error ) {
80+ func ( p Pass ) runPassHelper ( stdinContent string , args ... string ) (string , error ) {
5081 var stdout , stderr bytes.Buffer
5182 cmd := exec .Command ("pass" , args ... )
52- cmd .Stdin = strings .NewReader (stdin )
83+ cmd .Stdin = strings .NewReader (stdinContent )
5384 cmd .Stdout = & stdout
5485 cmd .Stderr = & stderr
5586
@@ -62,37 +93,26 @@ func runPass(stdin string, args ...string) (string, error) {
6293 return strings .TrimRight (stdout .String (), "\n \r " ), nil
6394}
6495
65- // Pass handles secrets using Linux secret-service as a store.
66- type Pass struct {}
67-
6896// Add adds new credentials to the keychain.
6997func (h Pass ) Add (creds * credentials.Credentials ) error {
70- if ! PassInitialized {
71- return errors .New ("pass store is uninitialized" )
72- }
73-
7498 if creds == nil {
7599 return errors .New ("missing credentials" )
76100 }
77101
78102 encoded := base64 .URLEncoding .EncodeToString ([]byte (creds .ServerURL ))
79103
80- _ , err := runPass (creds .Secret , "insert" , "-f" , "-m" , path .Join (PASS_FOLDER , encoded , creds .Username ))
104+ _ , err := h . runPass (creds .Secret , "insert" , "-f" , "-m" , path .Join (PASS_FOLDER , encoded , creds .Username ))
81105 return err
82106}
83107
84108// Delete removes credentials from the store.
85109func (h Pass ) Delete (serverURL string ) error {
86- if ! PassInitialized {
87- return errors .New ("pass store is uninitialized" )
88- }
89-
90110 if serverURL == "" {
91111 return errors .New ("missing server url" )
92112 }
93113
94114 encoded := base64 .URLEncoding .EncodeToString ([]byte (serverURL ))
95- _ , err := runPass ("" , "rm" , "-rf" , path .Join (PASS_FOLDER , encoded ))
115+ _ , err := h . runPass ("" , "rm" , "-rf" , path .Join (PASS_FOLDER , encoded ))
96116 return err
97117}
98118
@@ -124,10 +144,6 @@ func listPassDir(args ...string) ([]os.FileInfo, error) {
124144
125145// Get returns the username and secret to use for a given registry server URL.
126146func (h Pass ) Get (serverURL string ) (string , string , error ) {
127- if ! PassInitialized {
128- return "" , "" , errors .New ("pass store is uninitialized" )
129- }
130-
131147 if serverURL == "" {
132148 return "" , "" , errors .New ("missing server url" )
133149 }
@@ -152,16 +168,12 @@ func (h Pass) Get(serverURL string) (string, string, error) {
152168 }
153169
154170 actual := strings .TrimSuffix (usernames [0 ].Name (), ".gpg" )
155- secret , err := runPass ("" , "show" , path .Join (PASS_FOLDER , encoded , actual ))
171+ secret , err := h . runPass ("" , "show" , path .Join (PASS_FOLDER , encoded , actual ))
156172 return actual , secret , err
157173}
158174
159175// List returns the stored URLs and corresponding usernames for a given credentials label
160176func (h Pass ) List () (map [string ]string , error ) {
161- if ! PassInitialized {
162- return nil , errors .New ("pass store is uninitialized" )
163- }
164-
165177 servers , err := listPassDir ()
166178 if err != nil {
167179 return nil , err
0 commit comments