Vendor out the keybase pinentry

This commit is contained in:
Bernd Schoolmann 2024-02-03 22:55:49 +01:00
parent f60fcfd408
commit e0cb8a9187
No known key found for this signature in database
10 changed files with 622 additions and 2 deletions

View File

@ -6,8 +6,8 @@ import (
"errors"
"github.com/keybase/client/go/logger"
"github.com/keybase/client/go/pinentry"
"github.com/keybase/client/go/protocol/keybase1"
pinentry "github.com/quexten/goldwarden/agent/systemauth/pinentry/keybase-pinentry"
)
func GetPassword(title string, description string) (string, error) {

View File

@ -0,0 +1,27 @@
Copyright (c) 2015, Keybase
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of keybase nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,219 @@
// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.
package pinentry
import (
"bufio"
"fmt"
"io"
"os"
"os/exec"
"strings"
"github.com/keybase/client/go/logger"
keybase1 "github.com/keybase/client/go/protocol/keybase1"
)
//
// some borrowed from here:
//
// https://github.com/bradfitz/camlistore/blob/master/pkg/misc/pinentry/pinentry.go
//
// Under the Apache 2.0 license
//
type Pinentry struct {
initRes *error
path string
term string
tty string
prog string
log logger.Logger
}
func New(envprog string, log logger.Logger, tty string) *Pinentry {
return &Pinentry{
prog: envprog,
log: log,
tty: tty,
}
}
func (pe *Pinentry) Init() (error, error) {
if pe.initRes != nil {
return *pe.initRes, nil
}
err, fatalerr := pe.FindProgram()
if err == nil {
pe.GetTerminalName()
}
pe.term = os.Getenv("TERM")
pe.initRes = &err
return err, fatalerr
}
func (pe *Pinentry) SetInitError(e error) {
pe.initRes = &e
}
func (pe *Pinentry) FindProgram() (error, error) {
prog := pe.prog
var err, fatalerr error
if len(prog) > 0 {
if err = canExec(prog); err == nil {
pe.path = prog
} else {
err = fmt.Errorf("Can't execute given pinentry program '%s': %s",
prog, err)
fatalerr = err
}
} else if prog, err = FindPinentry(pe.log); err == nil {
pe.path = prog
}
return err, fatalerr
}
func (pe *Pinentry) Get(arg keybase1.SecretEntryArg) (res *keybase1.SecretEntryRes, err error) {
pe.log.Debug("+ Pinentry::Get()")
// Do a lazy initialization
if err, _ = pe.Init(); err != nil {
return
}
inst := pinentryInstance{parent: pe}
defer inst.Close()
if err = inst.Init(); err != nil {
// We probably shouldn't try to use this thing again if we failed
// to set it up.
pe.SetInitError(err)
return
}
res, err = inst.Run(arg)
pe.log.Debug("- Pinentry::Get() -> %v", err)
return
}
func (pi *pinentryInstance) Close() {
pi.stdin.Close()
pi.cmd.Wait()
}
type pinentryInstance struct {
parent *Pinentry
cmd *exec.Cmd
stdout io.ReadCloser
stdin io.WriteCloser
br *bufio.Reader
}
func (pi *pinentryInstance) Set(cmd, val string, errp *error) {
if val == "" {
return
}
fmt.Fprintf(pi.stdin, "%s %s\n", cmd, val)
line, _, err := pi.br.ReadLine()
if err != nil {
*errp = err
return
}
if string(line) != "OK" {
*errp = fmt.Errorf("Response to " + cmd + " was " + string(line))
}
return
}
func (pi *pinentryInstance) Init() (err error) {
parent := pi.parent
parent.log.Debug("+ pinentryInstance::Init()")
pi.cmd = exec.Command(parent.path)
pi.stdin, _ = pi.cmd.StdinPipe()
pi.stdout, _ = pi.cmd.StdoutPipe()
if err = pi.cmd.Start(); err != nil {
parent.log.Warning("unexpected error running pinentry (%s): %s", parent.path, err)
return
}
pi.br = bufio.NewReader(pi.stdout)
lineb, _, err := pi.br.ReadLine()
if err != nil {
err = fmt.Errorf("Failed to get getpin greeting: %s", err)
return
}
line := string(lineb)
if !strings.HasPrefix(line, "OK") {
err = fmt.Errorf("getpin greeting didn't say 'OK', said: %q", line)
return
}
if len(parent.tty) > 0 {
parent.log.Debug("setting ttyname to %s", parent.tty)
pi.Set("OPTION", "ttyname="+parent.tty, &err)
if err != nil {
parent.log.Debug("error setting ttyname: %s", err)
}
}
if len(parent.term) > 0 {
parent.log.Debug("setting ttytype to %s", parent.term)
pi.Set("OPTION", "ttytype="+parent.term, &err)
if err != nil {
parent.log.Debug("error setting ttytype: %s", err)
}
}
parent.log.Debug("- pinentryInstance::Init() -> %v", err)
return
}
func descEncode(s string) string {
s = strings.Replace(s, "%", "%%", -1)
s = strings.Replace(s, "\n", "%0A", -1)
return s
}
func resDecode(s string) string {
s = strings.Replace(s, "%25", "%", -1)
return s
}
func (pi *pinentryInstance) Run(arg keybase1.SecretEntryArg) (res *keybase1.SecretEntryRes, err error) {
pi.Set("SETPROMPT", arg.Prompt, &err)
pi.Set("SETDESC", descEncode(arg.Desc), &err)
pi.Set("SETOK", arg.Ok, &err)
pi.Set("SETCANCEL", arg.Cancel, &err)
pi.Set("SETERROR", arg.Err, &err)
if err != nil {
return
}
fmt.Fprintf(pi.stdin, "GETPIN\n")
var lineb []byte
lineb, _, err = pi.br.ReadLine()
if err != nil {
err = fmt.Errorf("Failed to read line after GETPIN: %v", err)
return
}
line := string(lineb)
switch {
case strings.HasPrefix(line, "D "):
res = &keybase1.SecretEntryRes{Text: resDecode(line[2:])}
case strings.HasPrefix(line, "ERR 83886179 canceled") || strings.HasPrefix(line, "ERR 83886179 Operation cancelled"):
res = &keybase1.SecretEntryRes{Canceled: true}
case line == "OK":
res = &keybase1.SecretEntryRes{}
default:
return nil, fmt.Errorf("GETPIN response didn't start with D; got %q", line)
}
return
}

View File

@ -0,0 +1,111 @@
// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.
//go:build darwin || dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris
// +build darwin dragonfly freebsd linux nacl netbsd openbsd solaris
package pinentry
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/keybase/client/go/logger"
)
//
// some borrowed from here:
//
// https://github.com/bradfitz/camlistore/blob/master/pkg/misc/pinentry/pinentry.go
//
// Under the Apache 2.0 license
//
func canExec(s string) error {
fi, err := os.Stat(s)
if err != nil {
return err
}
mode := fi.Mode()
//
// Only consider non-directories that have at least one +x
// bit set.
//
// TODO: Recheck this on windows!
// See here for lookpath: http://golang.org/src/pkg/os/exec/lp_windows.go
//
// Similar to check from exec.LookPath below
// See here: http://golang.org/src/pkg/os/exec/lp_unix.go
//
if mode.IsDir() {
return fmt.Errorf("Program '%s' is a directory", s)
} else if int(mode)&0111 == 0 {
return fmt.Errorf("Program '%s' isn't executable", s)
} else {
return nil
}
}
func FindPinentry(log logger.Logger) (string, error) {
if !HasWindows() {
return "", fmt.Errorf("Can't spawn gui window, not using pinentry")
}
bins := []string{
// If you install MacTools you'll wind up with this pinentry
"/usr/local/MacGPG2/libexec/pinentry-mac.app/Contents/MacOS/pinentry-mac",
}
extraPaths := []string{}
log.Debug("+ FindPinentry()")
cmds := []string{
"pinentry-gtk-2",
"pinentry-qt4",
"pinentry",
}
checkFull := func(s string) bool {
log.Debug("| Check fullpath %s", s)
found := (canExec(s) == nil)
if found {
log.Debug("- Found: %s", s)
}
return found
}
for _, b := range bins {
if checkFull(b) {
return b, nil
}
}
path := os.Getenv("PATH")
for _, c := range cmds {
log.Debug("| Looking for %s in standard PATH %s", c, path)
fullc, err := exec.LookPath(c)
if err == nil {
log.Debug("- Found %s", fullc)
return fullc, nil
}
}
for _, ep := range extraPaths {
for _, c := range cmds {
full := filepath.Join(ep, c)
if checkFull(full) {
return full, nil
}
}
}
log.Debug("- FindPinentry: none found")
return "", fmt.Errorf("No pinentry found, checked a bunch of different places")
}
func (pe *Pinentry) GetTerminalName() {
// Noop on all platforms but windows
}

View File

@ -0,0 +1,17 @@
// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.
//go:build !darwin
// +build !darwin
package pinentry
type pinentrySecretStoreInfo struct{}
func (pi *pinentryInstance) useSecretStore(useSecretStore bool) (pinentrySecretStoreInfo, error) {
return pinentrySecretStoreInfo{}, nil
}
func (pi *pinentryInstance) shouldStoreSecret(info pinentrySecretStoreInfo) bool {
return false
}

View File

@ -0,0 +1,117 @@
// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.
//go:build darwin
// +build darwin
package pinentry
import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"github.com/keybase/go-keychain"
)
const (
// pinentryServiceName is the service name that pinentry uses
// when storing into the Keychain.
pinentryServiceName = "GnuPG"
// accountNameByteLength is how many random bytes to use to
// generate the account name. 32 bytes of randomness is more
// than enough to make the account name unpredictable.
accountNameByteLength = 32
)
type pinentrySecretStoreInfo string
func (pi *pinentryInstance) useSecretStore(useSecretStore bool) (pinentrySecretStoreInfo, error) {
if !useSecretStore {
return "", nil
}
// Make account name unpredictable to make it infeasible for
// an attacker to guess (and thus sniff the passphrase). See
// https://github.com/keybase/client/issues/484#issuecomment-114313867
// .
var accountNameBytes [accountNameByteLength]byte
n, err := rand.Read(accountNameBytes[:])
if n != accountNameByteLength {
return "", fmt.Errorf("Unexpected random byte count %d", n)
}
if err != nil {
return "", err
}
accountName := "keybase-" + hex.EncodeToString(accountNameBytes[:])
// This will cause a "Save in Keychain" checkbox to appear in
// the pinentry dialog. If checked, pinentry will then save
// the entered passphrase into the keychain with the service
// name "GnuPG" and the account name equal to the passed-in
// cache-id option value.
pi.Set("OPTION", "cache-id "+accountName, &err)
if err != nil {
// It's possible that the pinentry being used doesn't support
// this option. So just return instead of causing a fatal
// error.
pi.parent.log.Debug("| Error setting pinentry cache-id OPTION: %s", err)
pi.parent.log.Debug("| Not using secret store as a result.")
return "", nil
}
return pinentrySecretStoreInfo(accountName), err
}
func (pi *pinentryInstance) shouldStoreSecret(info pinentrySecretStoreInfo) bool {
if len(info) == 0 {
return false
}
// We just want to know when the user did check the "Save in
// Keychain" checkbox, so remove whatever pinentry put into
// the keychain, and infer the state of the checkbox from the
// error (since there will be no error if an entry was found
// and deleted).
//
// This is a bit of a hack -- this may cause a dialog to pop
// up saying that the client wants to access the user's
// keychain. But this will do for now until we write our own
// pinentry.
query := keychain.NewItem()
query.SetSecClass(keychain.SecClassGenericPassword)
query.SetService(pinentryServiceName)
query.SetAccount(string(info))
query.SetMatchLimit(keychain.MatchLimitOne)
// We need to query and delete by item reference because the
// OSX keychain API only allows us to delete unowned items
// this way.
query.SetReturnRef(true)
ref, err := keychain.QueryItemRef(query)
if err != nil {
// Default to false if there was an error.
return false
}
if ref == nil {
// If not found, return false.
return false
}
defer keychain.Release(ref)
err = keychain.DeleteItemRef(ref)
if err != nil {
// Default to false if there was an error deleting.
return false
}
// Entry was found and deleted.
return true
}
func HasWindows() bool {
// We aren't in an ssh connection, so we can probably spawn a window.
return len(os.Getenv("SSH_CONNECTION")) == 0
}

View File

@ -0,0 +1,112 @@
// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.
//go:build windows
// +build windows
package pinentry
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/keybase/client/go/logger"
"golang.org/x/sys/windows/registry"
)
func HasWindows() bool {
// We're assuming you aren't using windows remotely.
return true
}
// LookPath searches for an executable binary named file
// in the directories named by the PATH environment variable.
// If file contains a slash, it is tried directly and the PATH is not consulted.
func canExec(s string) error {
if strings.IndexAny(s, `:\/`) == -1 {
s += string(filepath.Separator)
}
_, err := exec.LookPath(s)
return err
}
func FindPinentry(log logger.Logger) (string, error) {
// // If you install GPG you'll wind up with this pinentry
// C:\Program Files (x86)\GNU\GnuPG\pinentry-gtk-2.exe
// C:\Program Files (x86)\GNU\GnuPG\pinentry-qt4.exe
// C:\Program Files (x86)\GNU\GnuPG\pinentry-w32.exe
// C:\Program Files (x86)\GNU\GnuPG\pinentry.exe
k, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Wow6432Node\GNU\GnuPG`, registry.QUERY_VALUE)
if err != nil {
k, err = registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\GNU\GnuPG`, registry.QUERY_VALUE)
}
if err != nil {
log.Debug("- FindPinentry: can't open registry")
}
defer k.Close()
installDir, _, err := k.GetStringValue("Install Directory")
if err != nil {
log.Debug("- FindPinentry: can't get string from registry")
}
extraPaths := []string{}
log.Debug("+ FindPinentry()")
cmds := []string{
"pinentry-gtk-2.exe",
"pinentry-qt4.exe",
"pinentry-w32.exe",
"pinentry.exe",
}
// First, look where the registry points
for _, c := range cmds {
full := filepath.Join(installDir, c)
log.Debug("| (registry) Looking for %s", full)
_, err := exec.LookPath(full)
if err == nil {
return full, nil
}
}
// Look in program files, just in case
extraPaths = append(extraPaths, os.Getenv("ProgramFiles"))
extraPaths = append(extraPaths, os.Getenv("ProgramFiles(x86)"))
for _, ep := range extraPaths {
for _, c := range cmds {
full := filepath.Join(ep, "GNU", "GnuPG", c)
log.Debug("| Looking for %s", full)
_, err := exec.LookPath(full)
if err == nil {
return full, nil
}
}
}
for _, ep := range extraPaths {
for _, c := range cmds {
full := filepath.Join(ep, "Gpg4win", "bin", c)
log.Debug("| Looking for %s", full)
_, err := exec.LookPath(full)
if err == nil {
return full, nil
}
}
}
log.Debug("- FindPinentry: none found")
return "", fmt.Errorf("No pinentry found, checked a bunch of different places")
}
func (pe *Pinentry) GetTerminalName() {
pe.tty = "windows"
}

View File

@ -0,0 +1,14 @@
// Copyright 2015 Keybase, Inc. All rights reserved. Use of
// this source code is governed by the included BSD license.
//go:build dragonfly || freebsd || linux || nacl || netbsd || openbsd || solaris
// +build dragonfly freebsd linux nacl netbsd openbsd solaris
package pinentry
import "os"
func HasWindows() bool {
//If there is a DISPLAY then we can spawn a window to it.
return len(os.Getenv("DISPLAY")) > 0
}

3
go.mod
View File

@ -11,6 +11,7 @@ require (
github.com/gorilla/websocket v1.5.1
github.com/icza/gox v0.0.0-20230924165045-adcb03233bb5
github.com/keybase/client/go v0.0.0-20240202160538-668db6be75e4
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6
github.com/lox/go-touchid v0.0.0-20170712105233-619cc8e578d0
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
github.com/mitchellh/go-ps v1.0.0
@ -20,6 +21,7 @@ require (
github.com/vmihailenco/msgpack/v5 v5.4.1
golang.org/x/crypto v0.17.0
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848
golang.org/x/sys v0.15.0
)
require (
@ -32,7 +34,6 @@ require (
github.com/keybase/go-logging v0.0.0-20231213204715-4b3ff33ba5b6 // indirect
github.com/keybase/msgpackzip v0.0.0-20221220225959-4abf538d2b9c // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
)
require (

2
go.sum
View File

@ -41,6 +41,8 @@ github.com/keybase/go-framed-msgpack-rpc v0.0.0-20230103225103-1f052922b096 h1:r
github.com/keybase/go-framed-msgpack-rpc v0.0.0-20230103225103-1f052922b096/go.mod h1:XO67nMjltHJ8OsBWnFiDU1F67wR+rtJB21NXtb1TKyA=
github.com/keybase/go-jsonw v0.0.0-20200325173637-df90f282c233 h1:zLk+cB/0ShMCBcgBOXYgellLZiZahXFicJleKyrlqiM=
github.com/keybase/go-jsonw v0.0.0-20200325173637-df90f282c233/go.mod h1:lofKQwj13L0/7ji5VYaY0257JDlQE2BRRf+rI2Vk1rU=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs=
github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw=
github.com/keybase/go-logging v0.0.0-20231213204715-4b3ff33ba5b6 h1:H4IvZdHXpeK963LgCMbTcEviEal4891UGf2iOqOGL94=
github.com/keybase/go-logging v0.0.0-20231213204715-4b3ff33ba5b6/go.mod h1:0yOEB+QF1Ega1Cr7oMKb3yUAc3C9/eg6fBHB5HLP7AA=
github.com/keybase/msgpackzip v0.0.0-20221220225959-4abf538d2b9c h1:PRG2AXSelSy7MiDI+PwJR2QSqI1N3OybRUutsMiHtpo=