fix(detectors/privatekey): report encrypted keys with uncrackable passphrases (#5127)

When a private key is passphrase-protected and Crack cannot recover the
passphrase from the built-in wordlist, FromData set a verification error
on the result but then continued past the append, dropping the finding
entirely. An encrypted key committed to a repository is still a real
exposure (offline-crackable, weak or legacy KDFs, and the passphrase is
often reused or committed nearby), so it should surface as an unverified
finding.

Append the result before continuing so the encrypted key is reported as
unverified with its verification error and encrypted:true metadata,
matching how unencrypted keys are already reported under
--no-verification.

Closes #5115

Co-authored-by: Shahzad Haider <[email protected]>
This commit is contained in:
genisis0x
2026-07-29 11:37:34 +05:00
committed by GitHub
co-authored by Shahzad Haider
parent 6f3c981e7b
commit 67d0241853
2 changed files with 47 additions and 0 deletions
+6
View File
@@ -81,7 +81,13 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
s1.ExtraData["encrypted"] = "true"
parsedKey, passphrase, err = Crack([]byte(token))
if err != nil {
// The key is encrypted and the passphrase could not be
// recovered from the wordlist. It still represents a real
// exposure (offline-crackable, weak/legacy KDFs, passphrase
// often reused or committed nearby), so surface it as an
// unverified finding instead of dropping it silently.
s1.SetVerificationError(err, token)
results = append(results, s1)
continue
}
if passphrase != "" {
@@ -96,3 +96,44 @@ func TestPrivatekey_Pattern(t *testing.T) {
})
}
}
// encryptedUncrackablePattern is an ed25519 key protected by a passphrase that
// is not present in the built-in wordlist (pkg/detectors/privatekey/list.txt),
// so Crack cannot recover it.
var encryptedUncrackablePattern = `-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABDnvQyInG
vB+yh/WYczTGXbAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAILUSv84lMSLVN0iP
knFlHoYobo1oohtLNp/ihBaf76QxAAAAoErDjg2HQs1hgYm0vfyrpBxWrcJ1u/LQG4o6cm
GJ3NnZmMmJemrnMXjGHB3zj63AEPxxyb6BWZH5Olb5gYMgWcnOU91cGvyC7aT6C5cOnFb1
ZtkdxTxqeUOMFS51gwDT04aURSz/caCV9KN4Y2MCHyW+GWxxD7eL0R6KhyT2j0z2BewCw9
kRN1fq3C2rTb+wTaNz8q9X0R+6JGxfXGhpTJ0=
-----END OPENSSH PRIVATE KEY-----
`
// TestPrivatekey_EncryptedKeyReported asserts that a passphrase-protected key
// whose passphrase cannot be cracked is still surfaced as an unverified finding
// rather than being dropped silently (issue #5115).
func TestPrivatekey_EncryptedKeyReported(t *testing.T) {
d := Scanner{}
input := fmt.Sprintf("%s = '%s'", keyword, encryptedUncrackablePattern)
results, err := d.FromData(context.Background(), false, []byte(input))
if err != nil {
t.Fatalf("FromData error = %v", err)
}
if len(results) != 1 {
t.Fatalf("expected 1 result for an encrypted key with an uncrackable passphrase, got %d", len(results))
}
r := results[0]
if r.Verified {
t.Errorf("expected the finding to be unverified")
}
if r.VerificationError() == nil {
t.Errorf("expected a verification error to be set on the finding")
}
if r.ExtraData["encrypted"] != "true" {
t.Errorf("expected ExtraData[\"encrypted\"] = \"true\", got %q", r.ExtraData["encrypted"])
}
}