Skip to content

Commit 61f03db

Browse files
committed
cli/command/registry: refactor reading from stdin
Extract the code as a utility function, and add some GoDoc to describe the behavior. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
1 parent 9a41c73 commit 61f03db

File tree

2 files changed

+76
-6
lines changed

2 files changed

+76
-6
lines changed

cli/command/registry/login.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package registry
22

33
import (
4+
"bytes"
45
"context"
56
"errors"
67
"fmt"
@@ -88,6 +89,38 @@ func verifyLoginFlags(flags *pflag.FlagSet, opts loginOptions) error {
8889
return nil
8990
}
9091

92+
// readSecretFromStdin reads the secret from r and returns it as a string.
93+
// It trims terminal line-endings (LF, CRLF, or CR), which may be added when
94+
// inputting interactively or piping input. The value is otherwise treated as
95+
// opaque, preserving any other whitespace, including newlines, per [NIST SP 800-63B §5.1.1.2].
96+
// Note that trimming whitespace may still happen elsewhere (see [NIST SP 800-63B (revision 4) §3.1.1.2]);
97+
//
98+
// > Verifiers **MAY** make limited allowances for mistyping (e.g., removing
99+
// > leading and trailing whitespace characters before verification, allowing
100+
// > the verification of passwords with differing cases for the leading character)
101+
//
102+
// [NIST SP 800-63B §5.1.1.2]: https://pages.nist.gov/800-63-3/sp800-63b.html#memsecretver
103+
// [NIST SP 800-63B (revision 4) §3.1.1.2]: https://pages.nist.gov/800-63-4/sp800-63b.html#passwordver
104+
func readSecretFromStdin(r io.Reader) (string, error) {
105+
b, err := io.ReadAll(r)
106+
if err != nil {
107+
return "", err
108+
}
109+
if len(b) == 0 {
110+
return "", nil
111+
}
112+
113+
for _, eol := range [][]byte{[]byte("\r\n"), []byte("\n"), []byte("\r")} {
114+
var ok bool
115+
b, ok = bytes.CutSuffix(b, eol)
116+
if ok {
117+
break
118+
}
119+
}
120+
121+
return string(b), nil
122+
}
123+
91124
func verifyLoginOptions(dockerCLI command.Streams, opts *loginOptions) error {
92125
if opts.password != "" {
93126
_, _ = fmt.Fprintln(dockerCLI.Err(), "WARNING! Using --password via the CLI is insecure. Use --password-stdin.")
@@ -97,14 +130,14 @@ func verifyLoginOptions(dockerCLI command.Streams, opts *loginOptions) error {
97130
if opts.user == "" {
98131
return errors.New("username is empty")
99132
}
100-
101-
contents, err := io.ReadAll(dockerCLI.In())
133+
p, err := readSecretFromStdin(dockerCLI.In())
102134
if err != nil {
103135
return err
104136
}
105-
106-
opts.password = strings.TrimSuffix(string(contents), "\n")
107-
opts.password = strings.TrimSuffix(opts.password, "\r")
137+
if strings.TrimSpace(p) == "" {
138+
return errors.New("password is empty")
139+
}
140+
opts.password = p
108141
}
109142
return nil
110143
}

cli/command/registry/login_test.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"io"
99
"path/filepath"
1010
"testing"
11+
"testing/iotest"
1112
"time"
1213

1314
"github.com/creack/pty"
@@ -289,6 +290,38 @@ func TestRunLogin(t *testing.T) {
289290
},
290291
},
291292
},
293+
{
294+
doc: "password stdin empty",
295+
priorCredentials: map[string]configtypes.AuthConfig{},
296+
input: loginOptions{
297+
serverAddress: "reg1",
298+
user: "my-username",
299+
passwordStdin: true,
300+
},
301+
expectedErr: `password is empty`,
302+
expectedCredentials: map[string]configtypes.AuthConfig{
303+
"reg1": {
304+
Username: "my-username",
305+
ServerAddress: "reg1",
306+
},
307+
},
308+
},
309+
{
310+
doc: "password stdin read error",
311+
priorCredentials: map[string]configtypes.AuthConfig{},
312+
input: loginOptions{
313+
serverAddress: "reg1",
314+
user: "my-username",
315+
passwordStdin: true,
316+
},
317+
expectedErr: `TEST_READ_ERR`,
318+
expectedCredentials: map[string]configtypes.AuthConfig{
319+
"reg1": {
320+
Username: "my-username",
321+
ServerAddress: "reg1",
322+
},
323+
},
324+
},
292325
{
293326
doc: "password stdin with line-endings",
294327
priorCredentials: map[string]configtypes.AuthConfig{},
@@ -315,7 +348,11 @@ func TestRunLogin(t *testing.T) {
315348
cli := test.NewFakeCli(&fakeClient{})
316349
cli.SetConfigFile(cfg)
317350
if tc.input.passwordStdin {
318-
cli.SetIn(streams.NewIn(io.NopCloser(bytes.NewBufferString(tc.stdIn))))
351+
if tc.expectedErr == "TEST_READ_ERR" {
352+
cli.SetIn(streams.NewIn(io.NopCloser(iotest.ErrReader(errors.New(tc.expectedErr)))))
353+
} else {
354+
cli.SetIn(streams.NewIn(io.NopCloser(bytes.NewBufferString(tc.stdIn))))
355+
}
319356
}
320357

321358
for _, priorCred := range tc.priorCredentials {

0 commit comments

Comments
 (0)