Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow retries when decrypting keyfiles #64

Merged
merged 1 commit into from
Sep 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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