Skip to content
This repository has been archived by the owner on Jan 11, 2022. It is now read-only.

Commit

Permalink
Initial import
Browse files Browse the repository at this point in the history
Signed-off-by: Dominik Schulz <[email protected]>
  • Loading branch information
dominikschulz committed Mar 24, 2021
1 parent 4abd555 commit 027b4ad
Show file tree
Hide file tree
Showing 11 changed files with 744 additions and 1 deletion.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2021 Gopass
Copyright (c) 2021 Gopass Authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# pinentry

Pinentry client in Go
42 changes: 42 additions & 0 deletions cli/fallback.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cli

import (
"context"

"github.com/gopasspw/gopass/pkg/termio"
)

// Client is pinentry CLI drop-in
type Client struct{}

// New creates a new client
func New() (*Client, error) {
return &Client{}, nil
}

// Close is a no-op
func (c *Client) Close() {}

// Confirm is a no-op
func (c *Client) Confirm() bool {
return true
}

// Set is a no-op
func (c *Client) Set(string, string) error {
return nil
}

// Option is a no-op
func (c *Client) Option(string) error {
return nil
}

// GetPin prompts for the pin in the termnial and returns the output
func (c *Client) GetPin() ([]byte, error) {
pw, err := termio.AskForPassword(context.TODO(), "Please enter your PIN")
if err != nil {
return nil, err
}
return []byte(pw), nil
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/gopasspw/pinentry

go 1.16

require github.com/gopasspw/gopass v1.12.4
434 changes: 434 additions & 0 deletions go.sum

Large diffs are not rendered by default.

39 changes: 39 additions & 0 deletions gpgconf/gpgconf.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package gpgconf

import (
"bufio"
"bytes"
"os"
"os/exec"
"strings"

"github.com/gopasspw/gopass/pkg/debug"
)

// Path returns the path to a GPG component
func Path(key string) (string, error) {
buf := &bytes.Buffer{}
cmd := exec.Command("gpgconf")
cmd.Stdout = buf
cmd.Stderr = os.Stderr

debug.Log("%s %+v", cmd.Path, cmd.Args)
if err := cmd.Run(); err != nil {
return "", err
}

key = strings.TrimSpace(strings.ToLower(key))
sc := bufio.NewScanner(buf)
for sc.Scan() {
p := strings.Split(strings.TrimSpace(sc.Text()), ":")
if len(p) < 3 {
continue
}
if key == p[0] {
return p[2], nil
}
}

debug.Log("key %q not found", key)
return "", nil
}
164 changes: 164 additions & 0 deletions pinentry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// Package pinentry implements a cross platform pinentry client. It can be used
// to obtain credentials from the user through a simple UI application.
package pinentry

import (
"bufio"
"bytes"
"fmt"
"io"
"net/url"
"os"
"os/exec"
"strings"

"github.com/gopasspw/gopass/pkg/debug"
)

var (
// Unescape enables unescaping of percent encoded values,
// disabled by default for backward compatibility. See #1621
Unescape bool
)

func init() {
if sv := os.Getenv("GOPASS_PINENTRY_UNESCAPE"); sv == "on" || sv == "true" {
Unescape = true
}
}

// Client is a pinentry client
type Client struct {
cmd *exec.Cmd
in io.WriteCloser
out *bufio.Reader
}

// New creates a new pinentry client
func New() (*Client, error) {
cmd := exec.Command(GetBinary())
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, err
}

stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}

br := bufio.NewReader(stdout)
if err := cmd.Start(); err != nil {
return nil, err
}

// check welcome message
banner, _, err := br.ReadLine()
if err != nil {
return nil, err
}
if !bytes.HasPrefix(banner, []byte("OK")) {
return nil, fmt.Errorf("wrong banner: %s", banner)
}

cl := &Client{
cmd: cmd,
in: stdin,
out: br,
}

return cl, nil
}

// Close closes the client
func (c *Client) Close() {
_ = c.in.Close()
}

// Confirm sends the confirm message
func (c *Client) Confirm() bool {
if err := c.Set("confirm", ""); err == nil {
return true
}
return false
}

// Set sets a key
func (c *Client) Set(key, value string) error {
key = strings.ToUpper(key)
if value != "" {
value = " " + value
}
val := "SET" + key + value + "\n"
if _, err := c.in.Write([]byte(val)); err != nil {
return err
}
line, _, _ := c.out.ReadLine()
if string(line) != "OK" {
return fmt.Errorf("error: %s", line)
}
return nil
}

// Option sets an option, e.g. "default-cancel=Abbruch" or "allow-external-password-cache"
func (c *Client) Option(value string) error {
val := "OPTION " + value + "\n"
if _, err := c.in.Write([]byte(val)); err != nil {
return err
}
line, _, _ := c.out.ReadLine()
if string(line) != "OK" {
return fmt.Errorf("error: %s", line)
}
return nil
}

// GetPin asks for the pin
func (c *Client) GetPin() ([]byte, error) {
if _, err := c.in.Write([]byte("GETPIN\n")); err != nil {
return nil, err
}
buf, _, err := c.out.ReadLine()
if err != nil {
return nil, err
}
if bytes.HasPrefix(buf, []byte("OK")) {
return nil, nil
}
// handle status messages
for bytes.HasPrefix(buf, []byte("S ")) {
debug.Log("message: %q", string(buf))
buf, _, err = c.out.ReadLine()
if err != nil {
return nil, err
}
}
// now there should be some data
if !bytes.HasPrefix(buf, []byte("D ")) {
return nil, fmt.Errorf("unexpected response: %s", buf)
}

pin := make([]byte, len(buf))
if n := copy(pin, buf); n != len(buf) {
return nil, fmt.Errorf("failed to copy pin: expected %d bytes only copied %d", len(buf), n)
}

ok, _, err := c.out.ReadLine()
if err != nil {
return nil, err
}
if !bytes.HasPrefix(ok, []byte("OK")) {
return nil, fmt.Errorf("unexpected response: %s", ok)
}
pin = pin[2:]

// Handle unescaping, if enabled
if bytes.Contains(pin, []byte("%")) && Unescape {
sv, err := url.QueryUnescape(string(pin))
if err != nil {
return nil, fmt.Errorf("failed to unescape pin: %q", err)
}
pin = []byte(sv)
}
return pin, nil
}
13 changes: 13 additions & 0 deletions pinentry_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// +build darwin

package pinentry

import "github.com/gopasspw/pinentry/gpgconf"

// GetBinary always returns pinentry-mac
func GetBinary() string {
if p, err := gpgconf.Path("pinentry"); err == nil && p != "" {
return p
}
return "pinentry-mac"
}
13 changes: 13 additions & 0 deletions pinentry_others.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// +build !darwin,!windows

package pinentry

import "github.com/gopasspw/pinentry/gpgconf"

// GetBinary returns the binary name
func GetBinary() string {
if p, err := gpgconf.Path("pinentry"); err == nil && p != "" {
return p
}
return "pinentry"
}
19 changes: 19 additions & 0 deletions pinentry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package pinentry

import "fmt"

func ExampleClient() {
pi, err := New()
if err != nil {
panic(err)
}
_ = pi.Set("title", "Agent Pinentry")
_ = pi.Set("desc", "Asking for a passphrase")
_ = pi.Set("prompt", "Please enter your passphrase:")
_ = pi.Set("ok", "OK")
pin, err := pi.GetPin()
if err != nil {
panic(err)
}
fmt.Println(string(pin))
}
13 changes: 13 additions & 0 deletions pinentry_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// +build windows

package pinentry

import "github.com/gopasspw/pinentry/gpgconf"

// GetBinary always returns pinentry.exe
func GetBinary() string {
if p, err := gpgconf.Path("pinentry"); err == nil && p != "" {
return p
}
return "pinentry.exe"
}

0 comments on commit 027b4ad

Please sign in to comment.