@@ -14,42 +14,73 @@ import (
14
14
"os/exec"
15
15
"path"
16
16
"strings"
17
+ "sync"
17
18
18
19
"github.com/docker/docker-credential-helpers/credentials"
19
20
)
20
21
21
22
const PASS_FOLDER = "docker-credential-helpers"
22
23
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
+ }
26
42
27
- func init () {
43
+ func (p Pass ) checkInitialized () error {
44
+ initializationMutex .Lock ()
45
+ defer initializationMutex .Unlock ()
46
+ if passInitialized {
47
+ return nil
48
+ }
28
49
// In principle, we could just run `pass init`. However, pass has a bug
29
50
// where if gpg fails, it doesn't always exit 1. Additionally, pass
30
51
// uses gpg2, but gpg is the default, which may be confusing. So let's
31
52
// just explictily check that pass actually can store and retreive a
32
53
// password.
33
54
password := "pass is initialized"
34
- name := path .Join (PASS_FOLDER , "docker-pass-initialized-check" )
55
+ name := path .Join (getPassDir () , "docker-pass-initialized-check" )
35
56
36
- _ , err := runPass (password , "insert" , "-f" , "-m" , name )
57
+ _ , err := p . runPassHelper (password , "insert" , "-f" , "-m" , name )
37
58
if err != nil {
38
- return
59
+ return fmt . Errorf ( "error initializing pass: %v" , err )
39
60
}
40
61
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
+ }
43
72
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
46
76
}
77
+ return p .runPassHelper (stdinContent , args ... )
47
78
}
48
79
49
- func runPass ( stdin string , args ... string ) (string , error ) {
80
+ func ( p Pass ) runPassHelper ( stdinContent string , args ... string ) (string , error ) {
50
81
var stdout , stderr bytes.Buffer
51
82
cmd := exec .Command ("pass" , args ... )
52
- cmd .Stdin = strings .NewReader (stdin )
83
+ cmd .Stdin = strings .NewReader (stdinContent )
53
84
cmd .Stdout = & stdout
54
85
cmd .Stderr = & stderr
55
86
@@ -62,37 +93,26 @@ func runPass(stdin string, args ...string) (string, error) {
62
93
return strings .TrimRight (stdout .String (), "\n \r " ), nil
63
94
}
64
95
65
- // Pass handles secrets using Linux secret-service as a store.
66
- type Pass struct {}
67
-
68
96
// Add adds new credentials to the keychain.
69
97
func (h Pass ) Add (creds * credentials.Credentials ) error {
70
- if ! PassInitialized {
71
- return errors .New ("pass store is uninitialized" )
72
- }
73
-
74
98
if creds == nil {
75
99
return errors .New ("missing credentials" )
76
100
}
77
101
78
102
encoded := base64 .URLEncoding .EncodeToString ([]byte (creds .ServerURL ))
79
103
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 ))
81
105
return err
82
106
}
83
107
84
108
// Delete removes credentials from the store.
85
109
func (h Pass ) Delete (serverURL string ) error {
86
- if ! PassInitialized {
87
- return errors .New ("pass store is uninitialized" )
88
- }
89
-
90
110
if serverURL == "" {
91
111
return errors .New ("missing server url" )
92
112
}
93
113
94
114
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 ))
96
116
return err
97
117
}
98
118
@@ -124,10 +144,6 @@ func listPassDir(args ...string) ([]os.FileInfo, error) {
124
144
125
145
// Get returns the username and secret to use for a given registry server URL.
126
146
func (h Pass ) Get (serverURL string ) (string , string , error ) {
127
- if ! PassInitialized {
128
- return "" , "" , errors .New ("pass store is uninitialized" )
129
- }
130
-
131
147
if serverURL == "" {
132
148
return "" , "" , errors .New ("missing server url" )
133
149
}
@@ -152,16 +168,12 @@ func (h Pass) Get(serverURL string) (string, string, error) {
152
168
}
153
169
154
170
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 ))
156
172
return actual , secret , err
157
173
}
158
174
159
175
// List returns the stored URLs and corresponding usernames for a given credentials label
160
176
func (h Pass ) List () (map [string ]string , error ) {
161
- if ! PassInitialized {
162
- return nil , errors .New ("pass store is uninitialized" )
163
- }
164
-
165
177
servers , err := listPassDir ()
166
178
if err != nil {
167
179
return nil , err
0 commit comments