Skip to content

Commit 2a9e239

Browse files
committed
Add pin-prompt option
This commit adds the pin-prompt option to the Pin method to allow a user to ask for a pin using the terminal. Fixes smallstep/cli#1460
1 parent 592ab4d commit 2a9e239

File tree

4 files changed

+130
-7
lines changed

4 files changed

+130
-7
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ require (
2626
golang.org/x/crypto v0.40.0
2727
golang.org/x/net v0.42.0
2828
golang.org/x/sys v0.34.0
29+
golang.org/x/term v0.33.0
2930
google.golang.org/api v0.240.0
3031
google.golang.org/grpc v1.73.0
3132
google.golang.org/protobuf v1.36.6

internal/termutil/termutil.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2021 The age Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
//nolint:gocritic // this file is borrowed from age
6+
package termutil
7+
8+
import (
9+
"fmt"
10+
"io"
11+
"os"
12+
"runtime"
13+
14+
"golang.org/x/term"
15+
)
16+
17+
// clearLine clears the current line on the terminal, or opens a new line if
18+
// terminal escape codes don't work.
19+
func clearLine(out io.Writer) {
20+
const (
21+
CUI = "\033[" // Control Sequence Introducer
22+
CPL = CUI + "F" // Cursor Previous Line
23+
EL = CUI + "K" // Erase in Line
24+
)
25+
26+
// First, open a new line, which is guaranteed to work everywhere. Then, try
27+
// to erase the line above with escape codes.
28+
//
29+
// (We use CRLF instead of LF to work around an apparent bug in WSL2's
30+
// handling of CONOUT$. Only when running a Windows binary from WSL2, the
31+
// cursor would not go back to the start of the line with a simple LF.
32+
// Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.)
33+
fmt.Fprintf(out, "\r\n"+CPL+EL)
34+
}
35+
36+
// withTerminal runs f with the terminal input and output files, if available.
37+
// withTerminal does not open a non-terminal stdin, so the caller does not need
38+
// to check stdinInUse.
39+
func withTerminal(f func(in, out *os.File) error) error {
40+
if runtime.GOOS == "windows" {
41+
in, err := os.OpenFile("CONIN$", os.O_RDWR, 0)
42+
if err != nil {
43+
return err
44+
}
45+
defer in.Close()
46+
out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0)
47+
if err != nil {
48+
return err
49+
}
50+
defer out.Close()
51+
return f(in, out)
52+
}
53+
54+
var (
55+
tty *os.File
56+
err error
57+
)
58+
if tty, err = os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
59+
defer tty.Close()
60+
return f(tty, tty)
61+
}
62+
63+
if term.IsTerminal(int(os.Stdin.Fd())) {
64+
return f(os.Stdin, os.Stdin)
65+
}
66+
67+
return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %w", err)
68+
}
69+
70+
// ReadPassword reads a value from the terminal with no echo. The prompt is
71+
// ephemeral.
72+
func ReadPassword(prompt string) (s []byte, err error) {
73+
err = withTerminal(func(in, out *os.File) error {
74+
fmt.Fprintf(out, "%s ", prompt)
75+
defer clearLine(out)
76+
s, err = term.ReadPassword(int(in.Fd()))
77+
return err
78+
})
79+
return
80+
}

kms/uri/uri.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,13 @@ import (
1111
"unicode"
1212

1313
"github.com/pkg/errors"
14+
"go.step.sm/crypto/internal/termutil"
1415
)
1516

17+
// readPIN defines the method used to read a pin, it can be changed for testing
18+
// purposes.
19+
var readPIN = termutil.ReadPassword
20+
1621
// URI implements a parser for a URI format based on the the PKCS #11 URI Scheme
1722
// defined in https://tools.ietf.org/html/rfc7512
1823
//
@@ -191,6 +196,15 @@ func (u *URI) Pin() string {
191196
return string(bytes.TrimRightFunc(b, unicode.IsSpace))
192197
}
193198
}
199+
if u.Has("pin-prompt") {
200+
prompt := "Enter PIN:"
201+
if s := u.Get("pin-prompt"); s != "" {
202+
prompt = s
203+
}
204+
if b, err := readPIN(prompt); err == nil {
205+
return string(bytes.TrimRightFunc(b, unicode.IsSpace))
206+
}
207+
}
194208
return ""
195209
}
196210

kms/uri/uri_test.go

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

33
import (
4+
"errors"
45
"net/url"
56
"os"
67
"path/filepath"
@@ -275,18 +276,45 @@ func TestURI_GetEncoded(t *testing.T) {
275276
}
276277

277278
func TestURI_Pin(t *testing.T) {
279+
tmp := readPIN
280+
cleanup := func() {
281+
readPIN = tmp
282+
}
283+
278284
tests := []struct {
279-
name string
280-
uri *URI
281-
want string
285+
name string
286+
setup func(*testing.T)
287+
uri *URI
288+
want string
282289
}{
283-
{"from value", mustParse(t, "pkcs11:id=%72%73?pin-value=0123456789"), "0123456789"},
284-
{"from source", mustParse(t, "pkcs11:id=%72%73?pin-source=testdata/pin.txt"), "trim-this-pin"},
285-
{"from missing", mustParse(t, "pkcs11:id=%72%73"), ""},
286-
{"from source missing", mustParse(t, "pkcs11:id=%72%73?pin-source=testdata/foo.txt"), ""},
290+
{"from value", nil, mustParse(t, "pkcs11:id=%72%73?pin-value=0123456789"), "0123456789"},
291+
{"from source", nil, mustParse(t, "pkcs11:id=%72%73?pin-source=testdata/pin.txt"), "trim-this-pin"},
292+
{"from missing", nil, mustParse(t, "pkcs11:id=%72%73"), ""},
293+
{"from source missing", nil, mustParse(t, "pkcs11:id=%72%73?pin-source=testdata/foo.txt"), ""},
294+
{"from prompt", func(t *testing.T) {
295+
t.Cleanup(cleanup)
296+
readPIN = func(prompt string) (s []byte, err error) {
297+
return []byte("password"), nil
298+
}
299+
}, mustParse(t, "pkcs11:id=%72%73?pin-prompt"), "password"},
300+
{"from prompt with message", func(t *testing.T) {
301+
t.Cleanup(cleanup)
302+
readPIN = func(prompt string) (s []byte, err error) {
303+
return []byte("password \n"), nil
304+
}
305+
}, mustParse(t, "pkcs11:id=%72%73?pin-prompt=The+PIN+Please"), "password"},
306+
{"from prompt error", func(t *testing.T) {
307+
t.Cleanup(cleanup)
308+
readPIN = func(prompt string) (s []byte, err error) {
309+
return nil, errors.New("some error")
310+
}
311+
}, mustParse(t, "pkcs11:id=%72%73?pin-prompt"), ""},
287312
}
288313
for _, tt := range tests {
289314
t.Run(tt.name, func(t *testing.T) {
315+
if tt.setup != nil {
316+
tt.setup(t)
317+
}
290318
if got := tt.uri.Pin(); got != tt.want {
291319
t.Errorf("URI.Pin() = %v, want %v", got, tt.want)
292320
}

0 commit comments

Comments
 (0)