Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require (
golang.org/x/crypto v0.40.0
golang.org/x/net v0.42.0
golang.org/x/sys v0.34.0
golang.org/x/term v0.33.0
google.golang.org/api v0.240.0
google.golang.org/grpc v1.73.0
google.golang.org/protobuf v1.36.6
Expand Down
80 changes: 80 additions & 0 deletions internal/termutil/termutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// Copyright 2021 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//nolint:gocritic // this file is borrowed from age
package termutil

import (
"fmt"
"io"
"os"
"runtime"

"golang.org/x/term"
)

// clearLine clears the current line on the terminal, or opens a new line if
// terminal escape codes don't work.
func clearLine(out io.Writer) {
const (
CUI = "\033[" // Control Sequence Introducer
CPL = CUI + "F" // Cursor Previous Line
EL = CUI + "K" // Erase in Line
)

// First, open a new line, which is guaranteed to work everywhere. Then, try
// to erase the line above with escape codes.
//
// (We use CRLF instead of LF to work around an apparent bug in WSL2's
// handling of CONOUT$. Only when running a Windows binary from WSL2, the
// cursor would not go back to the start of the line with a simple LF.
// Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.)
fmt.Fprintf(out, "\r\n"+CPL+EL)
}

// withTerminal runs f with the terminal input and output files, if available.
// withTerminal does not open a non-terminal stdin, so the caller does not need
// to check stdinInUse.
func withTerminal(f func(in, out *os.File) error) error {
if runtime.GOOS == "windows" {
in, err := os.OpenFile("CONIN$", os.O_RDWR, 0)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0)
if err != nil {
return err
}
defer out.Close()
return f(in, out)
}

var (
tty *os.File
err error
)
if tty, err = os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
defer tty.Close()
return f(tty, tty)
}

if term.IsTerminal(int(os.Stdin.Fd())) {
return f(os.Stdin, os.Stdin)
}

return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %w", err)
}

// ReadPassword reads a value from the terminal with no echo. The prompt is
// ephemeral.
func ReadPassword(prompt string) (s []byte, err error) {
err = withTerminal(func(in, out *os.File) error {
fmt.Fprintf(out, "%s ", prompt)
defer clearLine(out)
s, err = term.ReadPassword(int(in.Fd()))
return err
})
return
}
14 changes: 14 additions & 0 deletions kms/uri/uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ import (
"unicode"

"github.com/pkg/errors"
"go.step.sm/crypto/internal/termutil"
)

// readPIN defines the method used to read a pin, it can be changed for testing
// purposes.
var readPIN = termutil.ReadPassword

// URI implements a parser for a URI format based on the the PKCS #11 URI Scheme
// defined in https://tools.ietf.org/html/rfc7512
//
Expand Down Expand Up @@ -191,6 +196,15 @@ func (u *URI) Pin() string {
return string(bytes.TrimRightFunc(b, unicode.IsSpace))
}
}
if u.Has("pin-prompt") {
prompt := "Enter PIN:"
if s := u.Get("pin-prompt"); s != "" {
prompt = s
}
if b, err := readPIN(prompt); err == nil {
return string(bytes.TrimRightFunc(b, unicode.IsSpace))
}
}
return ""
}

Expand Down
42 changes: 35 additions & 7 deletions kms/uri/uri_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package uri

import (
"errors"
"net/url"
"os"
"path/filepath"
Expand Down Expand Up @@ -275,18 +276,45 @@ func TestURI_GetEncoded(t *testing.T) {
}

func TestURI_Pin(t *testing.T) {
tmp := readPIN
cleanup := func() {
readPIN = tmp
}

tests := []struct {
name string
uri *URI
want string
name string
setup func(*testing.T)
uri *URI
want string
}{
{"from value", mustParse(t, "pkcs11:id=%72%73?pin-value=0123456789"), "0123456789"},
{"from source", mustParse(t, "pkcs11:id=%72%73?pin-source=testdata/pin.txt"), "trim-this-pin"},
{"from missing", mustParse(t, "pkcs11:id=%72%73"), ""},
{"from source missing", mustParse(t, "pkcs11:id=%72%73?pin-source=testdata/foo.txt"), ""},
{"from value", nil, mustParse(t, "pkcs11:id=%72%73?pin-value=0123456789"), "0123456789"},
{"from source", nil, mustParse(t, "pkcs11:id=%72%73?pin-source=testdata/pin.txt"), "trim-this-pin"},
{"from missing", nil, mustParse(t, "pkcs11:id=%72%73"), ""},
{"from source missing", nil, mustParse(t, "pkcs11:id=%72%73?pin-source=testdata/foo.txt"), ""},
{"from prompt", func(t *testing.T) {
t.Cleanup(cleanup)
readPIN = func(prompt string) (s []byte, err error) {
return []byte("password"), nil
}
}, mustParse(t, "pkcs11:id=%72%73?pin-prompt"), "password"},
{"from prompt with message", func(t *testing.T) {
t.Cleanup(cleanup)
readPIN = func(prompt string) (s []byte, err error) {
return []byte("password \n"), nil
}
}, mustParse(t, "pkcs11:id=%72%73?pin-prompt=The+PIN+Please"), "password"},
{"from prompt error", func(t *testing.T) {
t.Cleanup(cleanup)
readPIN = func(prompt string) (s []byte, err error) {
return nil, errors.New("some error")
}
}, mustParse(t, "pkcs11:id=%72%73?pin-prompt"), ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.setup != nil {
tt.setup(t)
}
if got := tt.uri.Pin(); got != tt.want {
t.Errorf("URI.Pin() = %v, want %v", got, tt.want)
}
Expand Down