pax_global_header00006660000000000000000000000064141673741160014524gustar00rootroot0000000000000052 comment=77f783497e7173012d0629c730d86cc2a8db45b1 go-pinentry-0.2.0/000077500000000000000000000000001416737411600137765ustar00rootroot00000000000000go-pinentry-0.2.0/.github/000077500000000000000000000000001416737411600153365ustar00rootroot00000000000000go-pinentry-0.2.0/.github/workflows/000077500000000000000000000000001416737411600173735ustar00rootroot00000000000000go-pinentry-0.2.0/.github/workflows/main.yml000066400000000000000000000016511416737411600210450ustar00rootroot00000000000000name: Test on: pull_request: push: branches: - master tags: - v* jobs: test: runs-on: ubuntu-latest strategy: matrix: go-version: - 1.16.x - 1.17.x steps: - name: Set up Go uses: actions/setup-go@v1 with: go-version: ${{ matrix.go-version }} - name: Cache Go modules uses: actions/cache@v1 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go${{ matrix.go-version }}- - name: Checkout uses: actions/checkout@v2 - name: Build run: go build ./... - name: Test run: go test -race ./... lint: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Lint uses: golangci/golangci-lint-action@v2 with: version: v1.43go-pinentry-0.2.0/.golangci.yml000066400000000000000000000024001416737411600163560ustar00rootroot00000000000000linters: enable: - asciicheck - bidichk - bodyclose - contextcheck - deadcode - depguard - dogsled - dupl - durationcheck - errcheck - errname - errorlint - exhaustive - exportloopref - forcetypeassert - gci - gochecknoinits - gocognit - gocritic - gocyclo - godot - goerr113 - gofmt - gofumpt - goheader - goimports - gomoddirectives - gomodguard - goprintffuncname - gosec - gosimple - govet - ifshort - importas - ineffassign - ireturn - makezero - misspell - nakedret - nilerr - nilnil - noctx - nolintlint - prealloc - predeclared - promlinter - revive - rowserrcheck - sqlclosecheck - staticcheck - structcheck - stylecheck - tagliatelle - tenv - thelper - typecheck - unconvert - unparam - unused - varcheck - wastedassign - whitespace disable: - cyclop - exhaustivestruct - forbidigo - funlen - gochecknoglobals - goconst - godox - gomnd - lll - nestif - nlreturn - paralleltest - testpackage - tparallel - varnamelen - wrapcheck - wsl linters-settings: goimports: local-prefixes: github.com/twpayne/go-pinentry #issues: #exclude-rules: #- linters: #- scopelint #path: "_test\\.go" go-pinentry-0.2.0/LICENSE000066400000000000000000000020641416737411600150050ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2021 Tom Payne Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. go-pinentry-0.2.0/README.md000066400000000000000000000023151416737411600152560ustar00rootroot00000000000000# go-pinentry [![PkgGoDev](https://pkg.go.dev/badge/github.com/twpayne/go-pinentry)](https://pkg.go.dev/github.com/twpayne/go-pinentry) Package `pinentry` provides a client to [GnuPG's pinentry](https://www.gnupg.org/related_software/pinentry/index.html). ## Key Features * Support for all `pinentry` features. * Idiomatic Go API. * Well tested. ## Example ```go client, err := pinentry.NewClient( pinentry.WithBinaryNameFromGnuPGAgentConf(), pinentry.WithDesc("My description"), pinentry.WithGPGTTY(), pinentry.WithPrompt("My prompt:"), pinentry.WithTitle("My title") ) if err != nil { return err } defer client.Close() switch pin, fromCache, err := client.GetPIN(); { case pinentry.IsCancelled(err): fmt.Println("Cancelled") case err != nil: return err case fromCache: fmt.Printf("PIN: %s (from cache)\n", pin) default: fmt.Printf("PIN: %s\n", pin) } ``` ## Comparison with related packages Compared to [`github.com/gopasspw/pinentry`](https://github.com/gopasspw/pinentry), this package: * Implements all `pinentry` features. * Includes tests. * Implements a full parser of the underlying Assuan protocol for better compatibility with all `pinentry` implementations. ## License MIT go-pinentry-0.2.0/client_test.go000066400000000000000000000241011416737411600166400ustar00rootroot00000000000000//go:generate mockgen -destination=mockprocess_test.go -package=pinentry_test . Process package pinentry_test import ( "strconv" "testing" "time" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/twpayne/go-pinentry" ) func TestClientClose(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", nil) c, err := pinentry.NewClient( pinentry.WithProcess(p), ) require.NoError(t, err) p.expectClose() require.NoError(t, c.Close()) } func TestClientArgs(t *testing.T) { for i, tc := range []struct { clientOptions []pinentry.ClientOption expectedArgs []string }{ { clientOptions: []pinentry.ClientOption{ pinentry.WithArgs([]string{ "--arg1", "--arg2", }), }, expectedArgs: []string{ "--arg1", "--arg2", }, }, { clientOptions: []pinentry.ClientOption{ pinentry.WithDebug(), }, expectedArgs: []string{ "--debug", }, }, { clientOptions: []pinentry.ClientOption{ pinentry.WithNoGlobalGrab(), }, expectedArgs: []string{ "--no-global-grab", }, }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", tc.expectedArgs) clientOptions := []pinentry.ClientOption{pinentry.WithProcess(p)} clientOptions = append(clientOptions, tc.clientOptions...) c, err := pinentry.NewClient(clientOptions...) require.NoError(t, err) p.expectClose() require.NoError(t, c.Close()) }) } } func TestClientBinaryName(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry-test", nil) c, err := pinentry.NewClient( pinentry.WithBinaryName("pinentry-test"), pinentry.WithProcess(p), ) require.NoError(t, err) p.expectClose() require.NoError(t, c.Close()) } func TestClientCommands(t *testing.T) { for i, tc := range []struct { clientOptions []pinentry.ClientOption expectedCommand string }{ { clientOptions: []pinentry.ClientOption{ pinentry.WithCancel("cancel"), }, expectedCommand: "SETCANCEL cancel", }, { clientOptions: []pinentry.ClientOption{ pinentry.WithDesc("desc"), }, expectedCommand: "SETDESC desc", }, { clientOptions: []pinentry.ClientOption{ pinentry.WithError("error"), }, expectedCommand: "SETERROR error", }, { clientOptions: []pinentry.ClientOption{ pinentry.WithKeyInfo("keyinfo"), }, expectedCommand: "SETKEYINFO keyinfo", }, { clientOptions: []pinentry.ClientOption{ pinentry.WithNotOK("notok"), }, expectedCommand: "SETNOTOK notok", }, { clientOptions: []pinentry.ClientOption{ pinentry.WithOK("ok"), }, expectedCommand: "SETOK ok", }, { clientOptions: []pinentry.ClientOption{ pinentry.WithOption("option"), }, expectedCommand: "OPTION option", }, { clientOptions: []pinentry.ClientOption{ pinentry.WithOptions([]string{ "option", }), }, expectedCommand: "OPTION option", }, { clientOptions: []pinentry.ClientOption{ pinentry.WithPrompt("prompt"), }, expectedCommand: "SETPROMPT prompt", }, { clientOptions: []pinentry.ClientOption{ pinentry.WithQualityBarToolTip("qualitybartooltip"), }, expectedCommand: "SETQUALITYBAR_TT qualitybartooltip", }, { clientOptions: []pinentry.ClientOption{ pinentry.WithTimeout(time.Second), }, expectedCommand: "SETTIMEOUT 1", }, { clientOptions: []pinentry.ClientOption{ pinentry.WithTitle("title"), }, expectedCommand: "SETTITLE title", }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", nil) p.expectWritelnOK(tc.expectedCommand) clientOptions := []pinentry.ClientOption{pinentry.WithProcess(p)} clientOptions = append(clientOptions, tc.clientOptions...) c, err := pinentry.NewClient(clientOptions...) require.NoError(t, err) p.expectClose() require.NoError(t, c.Close()) }) } } func TestClientGetPIN(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", nil) c, err := pinentry.NewClient( pinentry.WithProcess(p), ) require.NoError(t, err) expectedPIN := "abc" expectedFromCache := false p.expectWriteln("GETPIN") p.expectReadLine("D " + expectedPIN) p.expectReadLine("OK") actualPIN, actualFromCache, err := c.GetPIN() require.NoError(t, err) assert.Equal(t, expectedPIN, actualPIN) assert.Equal(t, expectedFromCache, actualFromCache) p.expectClose() require.NoError(t, c.Close()) } func TestClientConfirm(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", nil) c, err := pinentry.NewClient( pinentry.WithProcess(p), ) require.NoError(t, err) expectedConfirm := true p.expectWriteln("CONFIRM confirm") p.expectReadLine("OK") actualConfirm, err := c.Confirm("confirm") require.NoError(t, err) assert.Equal(t, expectedConfirm, actualConfirm) p.expectClose() require.NoError(t, c.Close()) } func TestClientConfirmCancel(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", nil) c, err := pinentry.NewClient( pinentry.WithProcess(p), ) require.NoError(t, err) p.expectWriteln("CONFIRM confirm") p.expectReadLine("ERR 83886179 Operation cancelled ") actualConfirm, err := c.Confirm("confirm") require.Error(t, err) assert.True(t, pinentry.IsCancelled(err)) assert.Equal(t, false, actualConfirm) p.expectClose() require.NoError(t, c.Close()) } func TestClientGetPINCancel(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", nil) c, err := pinentry.NewClient( pinentry.WithProcess(p), ) require.NoError(t, err) p.expectWriteln("GETPIN") p.expectReadLine("ERR 83886179 Operation cancelled ") actualPIN, actualFromCache, err := c.GetPIN() require.Error(t, err) assert.True(t, pinentry.IsCancelled(err)) assert.Equal(t, "", actualPIN) assert.Equal(t, false, actualFromCache) p.expectClose() require.NoError(t, c.Close()) } func TestClientGetPINFromCache(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", nil) c, err := pinentry.NewClient( pinentry.WithProcess(p), ) require.NoError(t, err) expectedPIN := "abc" expectedFromCache := true p.expectWriteln("GETPIN") p.expectReadLine("S PASSWORD_FROM_CACHE") p.expectReadLine("D " + expectedPIN) p.expectReadLine("OK") actualPIN, actualFromCache, err := c.GetPIN() require.NoError(t, err) assert.Equal(t, expectedPIN, actualPIN) assert.Equal(t, expectedFromCache, actualFromCache) p.expectClose() require.NoError(t, c.Close()) } func TestClientGetPINQualityBar(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", nil) p.expectWritelnOK("SETQUALITYBAR") c, err := pinentry.NewClient( pinentry.WithProcess(p), pinentry.WithQualityBar(func(pin string) (int, bool) { return 10 * len(pin), true }), ) require.NoError(t, err) expectedPIN := "abc" expectedFromCache := false p.expectWriteln("GETPIN") p.expectReadLine("INQUIRE QUALITY a") p.expectWriteln("D 10") p.expectWriteln("END") p.expectReadLine("INQUIRE QUALITY ab") p.expectWriteln("D 20") p.expectWriteln("END") p.expectReadLine("INQUIRE QUALITY abc") p.expectWriteln("D 30") p.expectWriteln("END") p.expectReadLine("D abc") p.expectReadLine("OK") actualPIN, actualFromCache, err := c.GetPIN() require.NoError(t, err) assert.Equal(t, expectedPIN, actualPIN) assert.Equal(t, expectedFromCache, actualFromCache) p.expectClose() require.NoError(t, c.Close()) } func TestClientGetPINQualityBarCancel(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", nil) p.expectWritelnOK("SETQUALITYBAR") c, err := pinentry.NewClient( pinentry.WithProcess(p), pinentry.WithQualityBar(func(pin string) (int, bool) { return 0, false }), ) require.NoError(t, err) expectedPIN := "abc" expectedFromCache := false p.expectWriteln("GETPIN") p.expectReadLine("INQUIRE QUALITY a") p.expectWriteln("CAN") p.expectReadLine("INQUIRE QUALITY ab") p.expectWriteln("CAN") p.expectReadLine("INQUIRE QUALITY abc") p.expectWriteln("CAN") p.expectReadLine("D abc") p.expectReadLine("OK") actualPIN, actualFromCache, err := c.GetPIN() require.NoError(t, err) assert.Equal(t, expectedPIN, actualPIN) assert.Equal(t, expectedFromCache, actualFromCache) p.expectClose() require.NoError(t, c.Close()) } func TestClientGetPINineUnexpectedResponse(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", nil) c, err := pinentry.NewClient( pinentry.WithProcess(p), ) require.NoError(t, err) p.expectWriteln("GETPIN") p.expectReadLine("unexpected response") actualPIN, actualFromCache, err := c.GetPIN() require.Error(t, err) assert.ErrorIs(t, err, pinentry.UnexpectedResponseError{ Line: "unexpected response", }) assert.Equal(t, "", actualPIN) assert.Equal(t, false, actualFromCache) p.expectClose() require.NoError(t, c.Close()) } func TestClientReadLineIgnoreBlank(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", nil) p.expectReadLine("") p.expectReadLine("\t") p.expectReadLine("\n") p.expectReadLine(" ") c, err := pinentry.NewClient( pinentry.WithProcess(p), ) require.NoError(t, err) p.expectClose() require.NoError(t, c.Close()) } func TestClientReadLineIgnoreComment(t *testing.T) { p := newMockProcess(t) p.expectStart("pinentry", nil) p.expectReadLine("#") p.expectReadLine("# comment") c, err := pinentry.NewClient( pinentry.WithProcess(p), ) require.NoError(t, err) p.expectClose() require.NoError(t, c.Close()) } func newMockProcess(t *testing.T) *MockProcess { t.Helper() return NewMockProcess(gomock.NewController(t)) } func (p *MockProcess) expectClose() { p.expectWriteln("BYE") p.expectReadLine("OK closing connection") p.EXPECT().Close().Return(nil) } func (p *MockProcess) expectReadLine(line string) { p.EXPECT().ReadLine().Return([]byte(line), false, nil) } func (p *MockProcess) expectStart(name string, args []string) { p.EXPECT().Start(name, args).Return(nil) p.expectReadLine("OK Pleased to meet you") } func (p *MockProcess) expectWriteln(line string) { p.EXPECT().Write([]byte(line+"\n")).Return(len(line)+1, nil) } func (p *MockProcess) expectWritelnOK(line string) { p.expectWriteln(line) p.expectReadLine("OK") } go-pinentry-0.2.0/cmd/000077500000000000000000000000001416737411600145415ustar00rootroot00000000000000go-pinentry-0.2.0/cmd/pinentry-test/000077500000000000000000000000001416737411600173665ustar00rootroot00000000000000go-pinentry-0.2.0/cmd/pinentry-test/main.go000066400000000000000000000021741416737411600206450ustar00rootroot00000000000000package main import ( "fmt" "os" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/twpayne/go-pinentry" ) func run(logger *zerolog.Logger) error { client, err := pinentry.NewClient( pinentry.WithBinaryNameFromGnuPGAgentConf(), pinentry.WithDesc("My multiline\ndescription"), pinentry.WithGPGTTY(), pinentry.WithPrompt("My prompt:"), pinentry.WithQualityBar(func(s string) (int, bool) { quality := 5 * len(s) if len(s) < 5 { quality = -quality } return quality, true }), pinentry.WithTitle("My title"), pinentry.WithLogger(logger), ) if err != nil { return err } defer func() { if err := client.Close(); err != nil { logger.Err(err).Msg("close") } }() switch pin, fromCache, err := client.GetPIN(); { case pinentry.IsCancelled(err): fmt.Println("Cancelled") return err case err != nil: return err case fromCache: fmt.Printf("PIN: %s (from cache)\n", pin) default: fmt.Printf("PIN: %s\n", pin) } return nil } func main() { logger := log.Output(zerolog.NewConsoleWriter()) if err := run(&logger); err != nil { logger.Err(err).Msg("error") os.Exit(1) } } go-pinentry-0.2.0/gnupg.go000066400000000000000000000017151416737411600154510ustar00rootroot00000000000000package pinentry import ( "os" "path/filepath" "regexp" "runtime" ) var gnuPGAgentConfPINEntryProgramRx = regexp.MustCompile(`(?m)^\s*pinentry-program\s+(\S+)`) // WithBinaryNameFromGnuPGAgentConf sets the name of the pinentry binary by // reading ~/.gnupg/gpg-agent.conf, if it exists. func WithBinaryNameFromGnuPGAgentConf() (clientOption ClientOption) { clientOption = func(*Client) {} userHomeDir, err := os.UserHomeDir() if err != nil { return } data, err := os.ReadFile(filepath.Join(userHomeDir, ".gnupg", "gpg-agent.conf")) if err != nil { return } match := gnuPGAgentConfPINEntryProgramRx.FindSubmatch(data) if match == nil { return } return func(c *Client) { c.binaryName = string(match[1]) } } // WithGPGTTY sets the tty. func WithGPGTTY() ClientOption { if runtime.GOOS == "windows" { return nil } gpgTTY, ok := os.LookupEnv("GPG_TTY") if !ok { return nil } return WithCommandf("OPTION %s=%s", OptionTTYName, gpgTTY) } go-pinentry-0.2.0/go.mod000066400000000000000000000006211416737411600151030ustar00rootroot00000000000000module github.com/twpayne/go-pinentry go 1.17 require ( github.com/golang/mock v1.6.0 github.com/rs/zerolog v1.26.0 github.com/stretchr/testify v1.7.0 go.uber.org/multierr v1.7.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/atomic v1.7.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) go-pinentry-0.2.0/go.sum000066400000000000000000000135141416737411600151350ustar00rootroot00000000000000github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.26.0 h1:ORM4ibhEZeTeQlCojCK2kPz1ogAY4bGs4tD+SaAdGaE= github.com/rs/zerolog v1.26.0/go.mod h1:yBiM87lvSqX8h0Ww4sdzNSkVYZ8dL2xjZJG1lAuGZEo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= go-pinentry-0.2.0/mockprocess_test.go000066400000000000000000000052601416737411600177170ustar00rootroot00000000000000// Code generated by MockGen. DO NOT EDIT. // Source: github.com/twpayne/go-pinentry (interfaces: Process) // Package pinentry_test is a generated GoMock package. package pinentry_test import ( reflect "reflect" gomock "github.com/golang/mock/gomock" ) // MockProcess is a mock of Process interface. type MockProcess struct { ctrl *gomock.Controller recorder *MockProcessMockRecorder } // MockProcessMockRecorder is the mock recorder for MockProcess. type MockProcessMockRecorder struct { mock *MockProcess } // NewMockProcess creates a new mock instance. func NewMockProcess(ctrl *gomock.Controller) *MockProcess { mock := &MockProcess{ctrl: ctrl} mock.recorder = &MockProcessMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. func (m *MockProcess) EXPECT() *MockProcessMockRecorder { return m.recorder } // Close mocks base method. func (m *MockProcess) Close() error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Close") ret0, _ := ret[0].(error) return ret0 } // Close indicates an expected call of Close. func (mr *MockProcessMockRecorder) Close() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockProcess)(nil).Close)) } // ReadLine mocks base method. func (m *MockProcess) ReadLine() ([]byte, bool, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ReadLine") ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(bool) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // ReadLine indicates an expected call of ReadLine. func (mr *MockProcessMockRecorder) ReadLine() *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReadLine", reflect.TypeOf((*MockProcess)(nil).ReadLine)) } // Start mocks base method. func (m *MockProcess) Start(arg0 string, arg1 []string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Start", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } // Start indicates an expected call of Start. func (mr *MockProcessMockRecorder) Start(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockProcess)(nil).Start), arg0, arg1) } // Write mocks base method. func (m *MockProcess) Write(arg0 []byte) (int, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Write", arg0) ret0, _ := ret[0].(int) ret1, _ := ret[1].(error) return ret0, ret1 } // Write indicates an expected call of Write. func (mr *MockProcessMockRecorder) Write(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockProcess)(nil).Write), arg0) } go-pinentry-0.2.0/pinentry.go000066400000000000000000000276741416737411600162150ustar00rootroot00000000000000// Package pinentry provides a client to GnuPG's pinentry. // // See info pinentry. // See https://www.gnupg.org/related_software/pinentry/index.html. // See https://www.gnupg.org/documentation/manuals/assuan.pdf. package pinentry // FIXME add secure logging mode to avoid logging PIN import ( "bytes" "errors" "fmt" "regexp" "strconv" "time" "github.com/rs/zerolog" "go.uber.org/multierr" ) // Options. const ( OptionAllowExternalPasswordCache = "allow-external-password-cache" OptionDefaultOK = "default-ok" OptionDefaultCancel = "default-cancel" OptionDefaultPrompt = "default-prompt" OptionTTYName = "ttyname" OptionTTYType = "ttytype" OptionLCCType = "lc-ctype" ) // Error codes. const ( AssuanErrorCodeCancelled = 83886179 ) // An AssuanError is returned when an error is sent over the Assuan protocol. type AssuanError struct { Code int Description string } func (e *AssuanError) Error() string { return e.Description } // An UnexpectedResponseError is returned when an unexpected response is // received. type UnexpectedResponseError struct { Line string } func newUnexpectedResponseError(line []byte) UnexpectedResponseError { return UnexpectedResponseError{ Line: string(line), } } func (e UnexpectedResponseError) Error() string { return fmt.Sprintf("pinentry: unexpected response: %q", e.Line) } var errorRx = regexp.MustCompile(`\AERR (\d+) (.*)\z`) // A QualityFunc evaluates the quality of a password. It should return a value // between -100 and 100. The absolute value of the return value is used as the // quality. Negative values turn the quality bar red. The boolean return value // indicates whether the quality is valid. type QualityFunc func(string) (int, bool) // A Client is a pinentry client. type Client struct { binaryName string args []string commands []string process Process qualityFunc QualityFunc logger *zerolog.Logger } // A ClientOption sets an option on a Client. type ClientOption func(*Client) // WithArgs appends extra arguments to the pinentry command. func WithArgs(args []string) ClientOption { return func(c *Client) { c.args = append(c.args, args...) } } // WithBinaryName sets the name of the pinentry binary name. The default is // pinentry. func WithBinaryName(binaryName string) ClientOption { return func(c *Client) { c.binaryName = binaryName } } // WithCancel sets the cancel button text. func WithCancel(cancel string) ClientOption { return WithCommandf("SETCANCEL %s", escape(cancel)) } // WithCommand appends an Assuan command that is sent when the connection is // established. func WithCommand(command string) ClientOption { return func(c *Client) { c.commands = append(c.commands, command) } } // WithCommandf appends an Assuan command that is sent when the connection is // established, using fmt.Sprintf to format the command. func WithCommandf(format string, args ...interface{}) ClientOption { command := fmt.Sprintf(format, args...) return WithCommand(command) } // WithDebug tells the pinentry command to print debug messages. func WithDebug() ClientOption { return func(c *Client) { c.args = append(c.args, "--debug") } } // WithDesc sets the description text. func WithDesc(desc string) ClientOption { return WithCommandf("SETDESC %s", escape(desc)) } // WithError sets the error text. func WithError(err string) ClientOption { return WithCommandf("SETERROR %s", escape(err)) } // WithKeyInfo sets a stable key identifier for use with password caching. func WithKeyInfo(keyInfo string) ClientOption { return WithCommandf("SETKEYINFO %s", escape(keyInfo)) } // WithLogger sets the logger. func WithLogger(logger *zerolog.Logger) ClientOption { return func(c *Client) { c.logger = logger } } // WithNoGlobalGrab instructs pinentry to only grab the password when the window // is focused. func WithNoGlobalGrab() ClientOption { return func(c *Client) { c.args = append(c.args, "--no-global-grab") } } // WithNotOK sets the text of the non-affirmative response button. func WithNotOK(notOK string) ClientOption { return WithCommandf("SETNOTOK %s", escape(notOK)) } // WithOK sets the text of the OK button. func WithOK(ok string) ClientOption { return WithCommandf("SETOK %s", escape(ok)) } // WithOption sets an option. func WithOption(option string) ClientOption { return WithCommandf("OPTION %s", escape(option)) } // WithOptions sets multiple options. func WithOptions(options []string) ClientOption { return func(c *Client) { for _, option := range options { command := fmt.Sprintf("OPTION %s", escape(option)) c.commands = append(c.commands, command) } } } // WithProcess sets the process. func WithProcess(process Process) ClientOption { return func(c *Client) { c.process = process } } // WithPrompt sets the prompt. func WithPrompt(prompt string) ClientOption { return WithCommandf("SETPROMPT %s", escape(prompt)) } // WithQualityBar enables the quality bar. func WithQualityBar(qualityFunc QualityFunc) ClientOption { return func(c *Client) { c.commands = append(c.commands, "SETQUALITYBAR") c.qualityFunc = qualityFunc } } // WithQualityBarToolTip sets the quality bar tool tip. func WithQualityBarToolTip(qualityBarTT string) ClientOption { return WithCommandf("SETQUALITYBAR_TT %s", escape(qualityBarTT)) } // WithTimeout sets the timeout. func WithTimeout(timeout time.Duration) ClientOption { return WithCommandf("SETTIMEOUT %d", timeout/time.Second) } // WithTitle sets the title. func WithTitle(title string) ClientOption { return WithCommandf("SETTITLE %s", escape(title)) } // NewClient returns a new Client with the given options. func NewClient(options ...ClientOption) (c *Client, err error) { c = &Client{ binaryName: "pinentry", process: &execProcess{}, qualityFunc: func(string) (int, bool) { return 0, false }, } for _, option := range options { if option != nil { option(c) } } err = c.process.Start(c.binaryName, c.args) if err != nil { return } defer func() { if err != nil { err = multierr.Append(err, c.Close()) } }() var line []byte line, err = c.readLine() if err != nil { return } if !isOK(line) { err = newUnexpectedResponseError(line) return } for _, command := range c.commands { if err = c.command(command); err != nil { return } } return c, nil } // Close closes the connection to the pinentry process. func (c *Client) Close() (err error) { defer func() { err = multierr.Append(err, c.process.Close()) }() if err = c.writeLine("BYE"); err != nil { return } err = c.readOK() return } // Confirm asks the user for confirmation. func (c *Client) Confirm(option string) (bool, error) { command := "CONFIRM" if option != "" { command += " " + option } if err := c.writeLine(command); err != nil { return false, err } switch line, err := c.readLine(); { case err != nil: return false, err case isOK(line): return true, nil case bytes.Equal(line, []byte("ASSUAN_Not_Confirmed")): return false, nil default: return false, newUnexpectedResponseError(line) } } // GetPIN gets a PIN from the user. If the user cancels, an error is returned // which can be tested with IsCancelled. func (c *Client) GetPIN() (pin string, fromCache bool, err error) { if err = c.writeLine("GETPIN"); err != nil { return "", false, err } for { var line []byte switch line, err = c.readLine(); { case err != nil: return case isOK(line): return case isData(line): pin = getPIN(line[2:]) case bytes.Equal(line, []byte("S PASSWORD_FROM_CACHE")): fromCache = true case bytes.HasPrefix(line, []byte("INQUIRE QUALITY ")): pin = getPIN(line[16:]) if quality, ok := c.qualityFunc(pin); ok { if quality < -100 { quality = -100 } else if quality > 100 { quality = 100 } if err = c.writeLine(fmt.Sprintf("D %d", quality)); err != nil { return } if err = c.writeLine("END"); err != nil { return } } else { if err = c.writeLine("CAN"); err != nil { return } } default: err = newUnexpectedResponseError(line) return } } } // command writes a command and reads an OK response. func (c *Client) command(command string) error { if err := c.writeLine(command); err != nil { return err } return c.readOK() } // readLine reads a line, ignoring blank lines and comments. func (c *Client) readLine() ([]byte, error) { for { line, _, err := c.process.ReadLine() if err != nil { return nil, err } if c.logger != nil { c.logger.Err(err).Bytes("line", line).Msg("readLine") } switch { case isBlank(line): case isComment(line): case isError(line): return nil, newError(line) default: return line, err } } } // readOK reads an OK response. func (c *Client) readOK() error { switch line, err := c.readLine(); { case err != nil: return err case isOK(line): return nil default: return newUnexpectedResponseError(line) } } // writeLine writes a single line. func (c *Client) writeLine(line string) error { _, err := c.process.Write([]byte(line + "\n")) if c.logger != nil { c.logger.Err(err).Str("line", line).Msg("write") } return err } // IsCancelled returns if the error is operation cancelled. func IsCancelled(err error) bool { var assuanError *AssuanError if !errors.As(err, &assuanError) { return false } return assuanError.Code == AssuanErrorCodeCancelled } func escape(s string) string { bytes := []byte(s) escapedBytes := make([]byte, 0, len(bytes)) for _, b := range bytes { switch b { case '\n': escapedBytes = append(escapedBytes, '%', '0', 'A') case '\r': escapedBytes = append(escapedBytes, '%', '0', 'D') case '%': escapedBytes = append(escapedBytes, '%', '2', '5') default: escapedBytes = append(escapedBytes, b) } } return string(escapedBytes) } // getPIN parses a PIN from suffix. func getPIN(data []byte) string { return string(unescape(data)) } // isBlank returns if line is blank. func isBlank(line []byte) bool { return len(bytes.TrimSpace(line)) == 0 } // isComment returns if line is a comment. func isComment(line []byte) bool { return bytes.HasPrefix(line, []byte("#")) } // isData returns if line is a data line. func isData(line []byte) bool { return bytes.HasPrefix(line, []byte("D ")) } // isError returns if line is an error. func isError(line []byte) bool { return bytes.HasPrefix(line, []byte("ERR ")) } // isOK returns if the line is an OK response. func isOK(line []byte) bool { return bytes.HasPrefix(line, []byte("OK")) } // isUppercaseHexDigit returns if c is an uppercase hexadecimal digit. func isUppercaseHexDigit(c byte) bool { switch { case '0' <= c && c <= '9': return true case 'A' <= c && c <= 'F': return true default: return false } } // newError returns an error parsed from line. func newError(line []byte) error { match := errorRx.FindSubmatch(line) if match == nil { return newUnexpectedResponseError(line) } code, _ := strconv.Atoi(string(match[1])) return &AssuanError{ Code: code, Description: string(match[2]), } } // unescape unescapes data, interpreting invalid escape sequences literally // rather than returning an error. // // This is to work around a bug in pinentry-mac 1.1.1 (and possibly earlier // versions) which does not escape the PIN in INQUIRE QUALITY messages to the // client. func unescape(data []byte) []byte { unescapedData := make([]byte, 0, len(data)) for i := 0; i < len(data); { if i < len(data)-2 && data[i] == '%' && isUppercaseHexDigit(data[i+1]) && isUppercaseHexDigit(data[i+2]) { c := (uppercaseHexDigitValue(data[i+1]) << 4) + uppercaseHexDigitValue(data[i+2]) unescapedData = append(unescapedData, c) i += 3 } else { unescapedData = append(unescapedData, data[i]) i++ } } return unescapedData } // uppercaseHexDigitValue returns the value of the uppercase hexadecimal digit // c. func uppercaseHexDigitValue(c byte) byte { switch { case '0' <= c && c <= '9': return c - '0' case 'A' <= c && c <= 'F': return c - 'A' + 0xA default: return 0 } } go-pinentry-0.2.0/pinentry_test.go000066400000000000000000000024541416737411600172410ustar00rootroot00000000000000package pinentry import ( "strconv" "testing" "github.com/stretchr/testify/assert" ) func TestEscapeUnescape(t *testing.T) { for i, tc := range []struct { unescaped string escaped string }{ { unescaped: "", escaped: "", }, { unescaped: "a", escaped: "a", }, { unescaped: "\n", escaped: "%0A", }, { unescaped: "\r", escaped: "%0D", }, { unescaped: "%", escaped: "%25", }, { unescaped: "a\r\n%b", escaped: "a%0D%0A%25b", }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { actualEscaped := escape(tc.unescaped) assert.Equal(t, tc.escaped, actualEscaped) actualUnescaped := unescape([]byte(tc.escaped)) assert.Equal(t, tc.unescaped, string(actualUnescaped)) }) } } func TestUnescape(t *testing.T) { for i, tc := range []struct { s string expectedUnescaped string }{ { s: "%", expectedUnescaped: "%", }, { s: "%0", expectedUnescaped: "%0", }, { s: "%0a", expectedUnescaped: "%0a", }, { s: "%0A%", expectedUnescaped: "\n%", }, } { t.Run(strconv.Itoa(i), func(t *testing.T) { actualUnescaped := unescape([]byte(tc.s)) assert.Equal(t, tc.expectedUnescaped, string(actualUnescaped)) }) } } go-pinentry-0.2.0/process.go000066400000000000000000000020231416737411600160000ustar00rootroot00000000000000package pinentry import ( "bufio" "io" "os/exec" "go.uber.org/multierr" ) // A Process abstracts the interface to a pinentry Process. type Process interface { io.WriteCloser ReadLine() ([]byte, bool, error) Start(string, []string) error } // A execProcess executes a pinentry process. type execProcess struct { cmd *exec.Cmd stdin io.WriteCloser stdout *bufio.Reader } func (p *execProcess) Close() (err error) { defer func() { err = multierr.Append(err, p.cmd.Wait()) }() err = p.stdin.Close() return } func (p *execProcess) ReadLine() ([]byte, bool, error) { return p.stdout.ReadLine() } func (p *execProcess) Start(name string, args []string) (err error) { p.cmd = exec.Command(name, args...) p.stdin, err = p.cmd.StdinPipe() if err != nil { return } var stdoutPipe io.ReadCloser stdoutPipe, err = p.cmd.StdoutPipe() if err != nil { return } p.stdout = bufio.NewReader(stdoutPipe) err = p.cmd.Start() return } func (p *execProcess) Write(data []byte) (int, error) { return p.stdin.Write(data) }