Skip to content

Commit

Permalink
feat: provide the UserID in pinentry prompt for a PGP keyfile
Browse files Browse the repository at this point in the history
  • Loading branch information
smlx committed Aug 8, 2021
1 parent 6b0c8dd commit 51d6056
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 56 deletions.
29 changes: 20 additions & 9 deletions internal/keyservice/gpg/keyfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

// keyfilePrivateKeys reads the given path and returns any private keys found.
func keyfilePrivateKeys(p string) ([]*packet.PrivateKey, error) {
func keyfilePrivateKeys(p string) ([]privateKeyfile, error) {
f, err := os.Open(p)
if err != nil {
return nil, fmt.Errorf("couldn't open path %s: %v", p, err)
Expand All @@ -23,15 +23,16 @@ func keyfilePrivateKeys(p string) ([]*packet.PrivateKey, error) {
}
switch {
case fileInfo.Mode().IsRegular():
return keysFromFile(f)
pk, err := keysFromFile(f)
return []privateKeyfile{*pk}, err
case fileInfo.IsDir():
// enumerate files in directory
dirents, err := f.ReadDir(0)
if err != nil {
return nil, fmt.Errorf("couldn't read directory")
}
// get any private keys from each file
var privKeys []*packet.PrivateKey
var privKeys []privateKeyfile
for _, dirent := range dirents {
direntInfo, err := dirent.Info()
if err != nil {
Expand All @@ -49,7 +50,7 @@ func keyfilePrivateKeys(p string) ([]*packet.PrivateKey, error) {
return nil,
fmt.Errorf("couldn't get keys from file %s: %v", subPath, err)
}
privKeys = append(privKeys, subPrivKeys...)
privKeys = append(privKeys, *subPrivKeys)
}
}
return privKeys, nil
Expand All @@ -59,9 +60,10 @@ func keyfilePrivateKeys(p string) ([]*packet.PrivateKey, error) {
}

// keysFromFile read a file and return any private keys found
func keysFromFile(f *os.File) ([]*packet.PrivateKey, error) {
func keysFromFile(f *os.File) (*privateKeyfile, error) {
var err error
var pkt packet.Packet
var uid *packet.UserId
var privKeys []*packet.PrivateKey
reader := packet.NewReader(f)
for pkt, err = reader.Next(); err != io.EOF; pkt, err = reader.Next() {
Expand All @@ -71,11 +73,20 @@ func keysFromFile(f *os.File) ([]*packet.PrivateKey, error) {
if err != nil {
return nil, fmt.Errorf("couldn't get next packet: %v", err)
}
k, ok := pkt.(*packet.PrivateKey)
if !ok {
switch k := pkt.(type) {
case *packet.PrivateKey:
privKeys = append(privKeys, k)
case *packet.UserId:
uid = k
default:
continue
}
privKeys = append(privKeys, k)
}
return privKeys, nil
if uid == nil {
uid = packet.NewUserId("n/a", "n/a", "n/a")
}
return &privateKeyfile{
uid: uid,
keys: privKeys,
}, nil
}
84 changes: 45 additions & 39 deletions internal/keyservice/gpg/keyservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,27 @@ import (

// PINEntryService provides an interface to talk to a pinentry program.
type PINEntryService interface {
GetPGPPassphrase(string) ([]byte, error)
GetPGPPassphrase(string, string) ([]byte, error)
}

type privateKeyfile struct {
uid *packet.UserId
keys []*packet.PrivateKey
}

// KeyService implements an interface for getting cryptographic keys from
// keyfiles on disk.
type KeyService struct {
// cache passphrases used for decryption
passphrases [][]byte
privKeys []*packet.PrivateKey
privKeys []privateKeyfile
log *zap.Logger
pinentry PINEntryService
}

// New returns a keyservice initialised with keys found at path.
// Path can be a file or directory.
func New(l *zap.Logger, pe PINEntryService,
path string) (*KeyService, error) {
func New(l *zap.Logger, pe PINEntryService, path string) (*KeyService, error) {
p, err := keyfilePrivateKeys(path)
if err != nil {
return nil, err
Expand Down Expand Up @@ -67,47 +71,49 @@ func (g *KeyService) HaveKey(keygrips [][]byte) (bool, []byte, error) {
func (g *KeyService) getKey(keygrip []byte) (*rsa.PrivateKey, error) {
var pass []byte
var err error
for _, k := range g.privKeys {
pubKey, ok := k.PublicKey.PublicKey.(*rsa.PublicKey)
if !ok {
continue
}
if !bytes.Equal(keygrip, keygripRSA(pubKey)) {
continue
}
if k.Encrypted {
// try existing passphrases
for _, pass := range g.passphrases {
if err = k.Decrypt(pass); err == nil {
g.log.Debug("decrypted using cached passphrase",
zap.String("fingerprint", k.KeyIdString()))
break
for _, pk := range g.privKeys {
for _, k := range pk.keys {
pubKey, ok := k.PublicKey.PublicKey.(*rsa.PublicKey)
if !ok {
continue
}
if !bytes.Equal(keygrip, keygripRSA(pubKey)) {
continue
}
if k.Encrypted {
// try existing passphrases
for _, pass := range g.passphrases {
if err = k.Decrypt(pass); err == nil {
g.log.Debug("decrypted using cached passphrase",
zap.String("fingerprint", k.KeyIdString()))
break
}
}
}
}
if k.Encrypted {
// ask for a passphrase
pass, err = g.pinentry.GetPGPPassphrase(
fmt.Sprintf("%X %X %X %X", k.Fingerprint[:5], k.Fingerprint[5:10],
k.Fingerprint[10:15], k.Fingerprint[15:]))
if err != nil {
return nil, fmt.Errorf("couldn't get passphrase for key %s: %v",
k.KeyIdString(), err)
if k.Encrypted {
// ask for a passphrase
pass, err = g.pinentry.GetPGPPassphrase(
fmt.Sprintf("%s (%s) <%s>", pk.uid.Name, pk.uid.Comment, pk.uid.Email),
fmt.Sprintf("%X %X %X %X", k.Fingerprint[:5], k.Fingerprint[5:10], k.Fingerprint[10:15], k.Fingerprint[15:]))
if err != nil {
return nil, fmt.Errorf("couldn't get passphrase for key %s: %v",
k.KeyIdString(), err)
}
g.passphrases = append(g.passphrases, pass)
if err = k.Decrypt(pass); err != nil {
return nil, fmt.Errorf("couldn't decrypt key %s: %v",
k.KeyIdString(), err)
}
g.log.Debug("decrypted using passphrase",
zap.String("fingerprint", k.KeyIdString()))
}
g.passphrases = append(g.passphrases, pass)
if err = k.Decrypt(pass); err != nil {
return nil, fmt.Errorf("couldn't decrypt key %s: %v",
privKey, ok := k.PrivateKey.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("not an RSA key %s: %v",
k.KeyIdString(), err)
}
g.log.Debug("decrypted using passphrase",
zap.String("fingerprint", k.KeyIdString()))
}
privKey, ok := k.PrivateKey.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("not an RSA key %s: %v",
k.KeyIdString(), err)
return privKey, nil
}
return privKey, nil
}
return nil, nil
}
Expand Down
2 changes: 1 addition & 1 deletion internal/keyservice/gpg/keyservice_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestGetSigner(t *testing.T) {
defer ctrl.Finish()
var mockPES = mock.NewMockPINEntryService(ctrl)
if tc.protected {
mockPES.EXPECT().GetPGPPassphrase(gomock.Any()).
mockPES.EXPECT().GetPGPPassphrase(gomock.Any(), gomock.Any()).
Return([]byte("trustno1"), nil)
}
ks, err := gpg.New(log, mockPES, tc.path)
Expand Down
8 changes: 4 additions & 4 deletions internal/mock/mock_keyservice.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions internal/pinentry/pinentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ type SecurityKey interface {
type PINEntry struct{}

// GetPGPPassphrase uses pinentry to get the passphrase of the key with the
// given keygrip.
func (*PINEntry) GetPGPPassphrase(fingerprint string) ([]byte, error) {
// given fingerprint.
func (*PINEntry) GetPGPPassphrase(userID, fingerprint string) ([]byte, error) {
p, err := pinentry.New()
if err != nil {
return []byte{}, fmt.Errorf("couldn't get pinentry client: %w", err)
Expand All @@ -33,7 +33,8 @@ func (*PINEntry) GetPGPPassphrase(fingerprint string) ([]byte, error) {
return nil,
fmt.Errorf("couldn't set prompt on passphrase pinentry: %w", err)
}
err = p.Set("desc", fmt.Sprintf("PGP key fingerprint: %s", fingerprint))
err = p.Set("desc", fmt.Sprintf("UserID: %s, Fingerprint: %s", userID,
fingerprint))
if err != nil {
return nil,
fmt.Errorf("couldn't set desc on passphrase pinentry: %w", err)
Expand Down

0 comments on commit 51d6056

Please sign in to comment.