diff --git a/internal/keyservice/gpg/keyservice.go b/internal/keyservice/gpg/keyservice.go index 19276f0..5ce41e3 100644 --- a/internal/keyservice/gpg/keyservice.go +++ b/internal/keyservice/gpg/keyservice.go @@ -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 { @@ -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 @@ -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())) diff --git a/internal/keyservice/gpg/keyservice_test.go b/internal/keyservice/gpg/keyservice_test.go index a4c2646..c1a6a5d 100644 --- a/internal/keyservice/gpg/keyservice_test.go +++ b/internal/keyservice/gpg/keyservice_test.go @@ -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) diff --git a/internal/mock/mock_keyservice.go b/internal/mock/mock_keyservice.go index c22e200..83db0af 100644 --- a/internal/mock/mock_keyservice.go +++ b/internal/mock/mock_keyservice.go @@ -33,17 +33,17 @@ func (m *MockPINEntryService) EXPECT() *MockPINEntryServiceMockRecorder { return m.recorder } -// GetPGPPassphrase mocks base method. -func (m *MockPINEntryService) GetPGPPassphrase(arg0, arg1 string) ([]byte, error) { +// GetPassphrase mocks base method. +func (m *MockPINEntryService) GetPassphrase(arg0, arg1 string, arg2 int) ([]byte, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPGPPassphrase", arg0, arg1) + ret := m.ctrl.Call(m, "GetPassphrase", arg0, arg1, arg2) ret0, _ := ret[0].([]byte) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetPGPPassphrase indicates an expected call of GetPGPPassphrase. -func (mr *MockPINEntryServiceMockRecorder) GetPGPPassphrase(arg0, arg1 interface{}) *gomock.Call { +// GetPassphrase indicates an expected call of GetPassphrase. +func (mr *MockPINEntryServiceMockRecorder) GetPassphrase(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPGPPassphrase", reflect.TypeOf((*MockPINEntryService)(nil).GetPGPPassphrase), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPassphrase", reflect.TypeOf((*MockPINEntryService)(nil).GetPassphrase), arg0, arg1, arg2) } diff --git a/internal/pinentry/pinentry.go b/internal/pinentry/pinentry.go index 24cc6e5..2740ae9 100644 --- a/internal/pinentry/pinentry.go +++ b/internal/pinentry/pinentry.go @@ -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) { @@ -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) @@ -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) @@ -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) diff --git a/internal/ssh/agent.go b/internal/ssh/agent.go index 37818d4..630bf31 100644 --- a/internal/ssh/agent.go +++ b/internal/ssh/agent.go @@ -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 } @@ -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 @@ -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