Skip to content

Commit

Permalink
Merge pull request #64 from smlx/retry-passphrase
Browse files Browse the repository at this point in the history
Allow retries when decrypting keyfiles
  • Loading branch information
smlx authored Sep 18, 2021
2 parents 767e776 + 8126bde commit a8c7be8
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 76 deletions.
43 changes: 29 additions & 14 deletions internal/keyservice/gpg/keyservice.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ import (
"golang.org/x/crypto/openpgp/packet"
)

// retries is the passphrase attempt limit when decrypting GPG keyfiles
const retries = 3

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

type privateKeyfile struct {
Expand Down Expand Up @@ -67,10 +70,32 @@ func (g *KeyService) HaveKey(keygrips [][]byte) (bool, []byte, error) {
return false, nil, nil
}

// doDecrypt prompts for a passphrase via pinentry and uses the passphrase to
// decrypt the given private key
func (g *KeyService) doDecrypt(k *packet.PrivateKey, uid string) error {
var pass []byte
var err error
for i := 0; i < retries; i++ {
pass, err = g.pinentry.GetPassphrase(
fmt.Sprintf("UserID: %s\rFingerprint: %X %X %X %X", uid,
k.Fingerprint[:5], k.Fingerprint[5:10], k.Fingerprint[10:15],
k.Fingerprint[15:]),
uid, retries-i)
if err != nil {
return fmt.Errorf("couldn't get passphrase for key %s: %v",
k.KeyIdString(), err)
}
if err = k.Decrypt(pass); err == nil {
g.passphrases = append(g.passphrases, pass)
return nil
}
}
return fmt.Errorf("couldn't decrypt key %s: %v", k.KeyIdString(), err)
}

// decryptPrivateKey decrypts the given private key.
// Returns nil if successful, or an error if the key could not be decrypted.
func (g *KeyService) decryptPrivateKey(k *packet.PrivateKey, uid string) error {
var pass []byte
var err error
if k.Encrypted {
// try existing passphrases
Expand All @@ -83,18 +108,8 @@ func (g *KeyService) decryptPrivateKey(k *packet.PrivateKey, uid string) error {
}
}
if k.Encrypted {
// ask for a passphrase
pass, err = g.pinentry.GetPGPPassphrase(uid,
fmt.Sprintf("%X %X %X %X", k.Fingerprint[:5], k.Fingerprint[5:10],
k.Fingerprint[10:15], k.Fingerprint[15:]))
if err != nil {
return 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 fmt.Errorf("couldn't decrypt key %s: %v",
k.KeyIdString(), err)
if err := g.doDecrypt(k, uid); err != nil {
return err
}
g.log.Debug("decrypted using passphrase",
zap.String("fingerprint", k.KeyIdString()))
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(), gomock.Any()).
mockPES.EXPECT().GetPassphrase(gomock.Any(), gomock.Any(), 3).
Return([]byte("trustno1"), nil)
}
ks, err := gpg.New(log, mockPES, tc.path)
Expand Down
12 changes: 6 additions & 6 deletions internal/mock/mock_keyservice.go

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

44 changes: 3 additions & 41 deletions internal/pinentry/pinentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,44 +15,6 @@ type SecurityKey interface {
// PINEntry implements useful pinentry service methods.
type PINEntry struct{}

// GetPGPPassphrase uses pinentry to get the passphrase of the key with the
// 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)
}
defer p.Close()
err = p.Set("title", "piv-agent Passphrase Prompt")
if err != nil {
return nil,
fmt.Errorf("couldn't set title on passphrase pinentry: %w", err)
}
err = p.Set("prompt", "Please enter passphrase")
if err != nil {
return nil,
fmt.Errorf("couldn't set prompt on passphrase pinentry: %w", err)
}
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)
}
// optional PIN cache
err = p.Option("allow-external-password-cache")
if err != nil {
return nil,
fmt.Errorf("couldn't set option on passphrase pinentry: %w", err)
}
err = p.Set("KEYINFO", fingerprint)
if err != nil {
return nil,
fmt.Errorf("couldn't set KEYINFO on passphrase pinentry: %w", err)
}
return p.GetPin()
}

// GetPin uses pinentry to get the pin of the given token.
func GetPin(k SecurityKey) func() (string, error) {
return func() (string, error) {
Expand Down Expand Up @@ -93,7 +55,7 @@ func GetPin(k SecurityKey) func() (string, error) {
}

// GetPassphrase uses pinentry to get the passphrase of the given key file.
func GetPassphrase(keyPath, fingerprint string) ([]byte, error) {
func (*PINEntry) GetPassphrase(desc, keyID string, tries int) ([]byte, error) {
p, err := pinentry.New()
if err != nil {
return []byte{}, fmt.Errorf("couldn't get pinentry client: %w", err)
Expand All @@ -110,7 +72,7 @@ func GetPassphrase(keyPath, fingerprint string) ([]byte, error) {
fmt.Errorf("couldn't set prompt on passphrase pinentry: %w", err)
}
err = p.Set("desc",
fmt.Sprintf("%s %s %s", keyPath, fingerprint[:25], fingerprint[25:]))
fmt.Sprintf("%s\r(%d attempts remaining)", desc, tries))
if err != nil {
return nil,
fmt.Errorf("couldn't set desc on passphrase pinentry: %w", err)
Expand All @@ -121,7 +83,7 @@ func GetPassphrase(keyPath, fingerprint string) ([]byte, error) {
return nil,
fmt.Errorf("couldn't set option on passphrase pinentry: %w", err)
}
err = p.Set("KEYINFO", fingerprint)
err = p.Set("KEYINFO", keyID)
if err != nil {
return nil,
fmt.Errorf("couldn't set KEYINFO on passphrase pinentry: %w", err)
Expand Down
50 changes: 36 additions & 14 deletions internal/ssh/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,16 @@ import (
"golang.org/x/crypto/ssh/agent"
)

// retries is the passphrase attempt limit when decrypting SSH keyfiles
const retries = 3

// Agent implements the crypto/ssh Agent interface
// https://pkg.go.dev/golang.org/x/crypto/ssh/agent#Agent
type Agent struct {
mu sync.Mutex
piv *piv.KeyService
log *zap.Logger
pinentry *pinentry.PINEntry
loadKeyfile bool
}

Expand Down Expand Up @@ -220,6 +224,35 @@ func (a *Agent) tokenSigners() ([]gossh.Signer, error) {
return signers, nil
}

// doDecrypt prompts for a passphrase via pinentry and uses the passphrase to
// decrypt the given private key
func (a *Agent) doDecrypt(keyPath string,
pub gossh.PublicKey, priv []byte) (gossh.Signer, error) {
var passphrase []byte
var signer gossh.Signer
var err error
for i := 0; i < retries; i++ {
passphrase = passphrases[string(pub.Marshal())]
if passphrase == nil {
fingerprint := gossh.FingerprintSHA256(pub)
passphrase, err = a.pinentry.GetPassphrase(
fmt.Sprintf("%s %s %s", keyPath, fingerprint[:25], fingerprint[25:]),
fingerprint, retries-i)
if err != nil {
return nil, err
}
}
signer, err = gossh.ParsePrivateKeyWithPassphrase(priv, passphrase)
if err == nil {
a.log.Debug("loaded key from disk",
zap.Binary("public key bytes", signer.PublicKey().Marshal()))
passphrases[string(signer.PublicKey().Marshal())] = passphrase
return signer, nil
}
}
return nil, fmt.Errorf("couldn't decrypt and parse private key %v", err)
}

// get signers for all keys stored in files on disk
func (a *Agent) keyfileSigners() ([]gossh.Signer, error) {
var signers []gossh.Signer
Expand All @@ -228,33 +261,22 @@ func (a *Agent) keyfileSigners() ([]gossh.Signer, error) {
return nil, err
}
keyPath := filepath.Join(home, ".ssh/id_ed25519")
privBytes, err := ioutil.ReadFile(keyPath)
priv, err := ioutil.ReadFile(keyPath)
if err != nil {
a.log.Debug("couldn't load keyfile", zap.String("path", keyPath),
zap.Error(err))
return signers, nil
}
signer, err := gossh.ParsePrivateKey(privBytes)
signer, err := gossh.ParsePrivateKey(priv)
if err != nil {
pmErr, ok := err.(*gossh.PassphraseMissingError)
if !ok {
return nil, err
}
passphrase := passphrases[string(pmErr.PublicKey.Marshal())]
if passphrase == nil {
passphrase, err = pinentry.GetPassphrase(keyPath,
string(gossh.FingerprintSHA256(pmErr.PublicKey)))
if err != nil {
return nil, err
}
}
signer, err = gossh.ParsePrivateKeyWithPassphrase(privBytes, passphrase)
signer, err = a.doDecrypt(keyPath, pmErr.PublicKey, priv)
if err != nil {
return nil, err
}
a.log.Debug("loaded key from disk",
zap.Binary("public key bytes", signer.PublicKey().Marshal()))
passphrases[string(signer.PublicKey().Marshal())] = passphrase
}
signers = append(signers, signer)
return signers, nil
Expand Down

0 comments on commit a8c7be8

Please sign in to comment.