Skip to content
Open
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
108 changes: 108 additions & 0 deletions pkg/detectors/base64privatekey/base64_private_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package base64privatekey

import (
"context"
"encoding/base64"
"strings"

regexp "github.com/wasilibs/go-re2"

"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)

type Scanner struct {
detectors.DefaultMultiPartCredentialProvider
}

var _ detectors.Detector = (*Scanner)(nil)
var _ detectors.CustomFalsePositiveChecker = (*Scanner)(nil)

var (
// Match base64 encoded strings that might contain private keys
// Look for base64 strings that decode to contain "BEGIN.*PRIVATE KEY"
base64Pat = regexp.MustCompile(`[A-Za-z0-9+/]{100,}={0,2}`)

// After decoding, check for private key markers
privateKeyMarkers = []string{
"BEGIN RSA PRIVATE KEY",
"BEGIN DSA PRIVATE KEY",
"BEGIN EC PRIVATE KEY",
"BEGIN PRIVATE KEY",
"BEGIN ENCRYPTED PRIVATE KEY",
"BEGIN OPENSSH PRIVATE KEY",
}
)

func (s Scanner) Keywords() []string {
return []string{"private", "key", "rsa", "BEGIN"}
}

func (s Scanner) Type() detector_typepb.DetectorType {
return detector_typepb.DetectorType_Base64PrivateKey
}

func (s Scanner) Description() string {
return "Base64-encoded private keys found in code can expose cryptographic credentials. These keys should be stored securely and never committed to version control."
}

func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (results []detectors.Result, err error) {
dataStr := string(data)

uniqueMatches := make(map[string]struct{})

// Find potential base64 strings
for _, match := range base64Pat.FindAllString(dataStr, -1) {
match = strings.TrimSpace(match)

// Try to decode
decoded, err := base64.StdEncoding.DecodeString(match)
if err != nil {
// Try URL encoding
decoded, err = base64.URLEncoding.DecodeString(match)
if err != nil {
continue
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URLEncoding fallback can never succeed for regex matches

Medium Severity

The base64.URLEncoding fallback on decode failure is unreachable dead logic. The regex base64Pat only matches standard base64 characters (+, /), but URLEncoding expects URL-safe characters (-, _). If the match contains + or /, URL decoding rejects them as invalid. If it doesn't, both encodings behave identically. Either way, URLEncoding can never succeed when StdEncoding fails for these matches. The most likely failure mode for StdEncoding is missing padding, so the correct fallback is base64.RawStdEncoding, which tolerates unpadded input. Without this, base64-encoded private keys stored without = padding are silently skipped.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bd56182. Configure here.


decodedStr := string(decoded)

// Check if decoded content contains private key markers
isPrivateKey := false
for _, marker := range privateKeyMarkers {
if strings.Contains(decodedStr, marker) {
isPrivateKey = true
break
}
}

if isPrivateKey {
uniqueMatches[match] = struct{}{}
}
}

for encodedKey := range uniqueMatches {
r := detectors.Result{
DetectorType: s.Type(),
Raw: []byte(encodedKey),
SecretParts: map[string]string{
"base64_key": encodedKey,
},
ExtraData: map[string]string{
"warning": "Base64-encoded private key detected. Decode to identify key type.",
},
}

// Private keys cannot be verified without knowing the corresponding public key or service
// So we just flag them as found
r.Verified = false

results = append(results, r)
}

return results, nil
}

func (s Scanner) IsFalsePositive(result detectors.Result) (bool, string) {
return detectors.IsKnownFalsePositive(string(result.Raw), detectors.DefaultFalsePositives, true)
}
165 changes: 165 additions & 0 deletions pkg/detectors/base64privatekey/base64_private_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package base64privatekey

import (
"context"
"encoding/base64"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/require"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine/ahocorasick"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detector_typepb"
)

func TestBase64PrivateKey_Pattern(t *testing.T) {
d := Scanner{}
ahoCorasickCore := ahocorasick.NewAhoCorasickCore([]detectors.Detector{d})

// Create test private keys and encode them
rsaPrivateKey := `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA0Z6ca1yP6DGV/t5Z3i3h4z3Y8oHs5vZ0N2TQzL1d5dX5w3e
-----END RSA PRIVATE KEY-----`

ecPrivateKey := `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIKj0p8D7bJxzd/h3h3h3h3h3h3h3h3h3h3h3h3h3h3
-----END EC PRIVATE KEY-----`

validBase64RSA := base64.StdEncoding.EncodeToString([]byte(rsaPrivateKey))
validBase64EC := base64.StdEncoding.EncodeToString([]byte(ecPrivateKey))

tests := []struct {
name string
input string
want []string
}{
{
name: "valid base64 RSA private key",
input: `PRIVATE_KEY="` + validBase64RSA + `"`,
want: []string{validBase64RSA},
},
{
name: "valid base64 EC private key",
input: `EC_KEY=` + validBase64EC,
want: []string{validBase64EC},
},
{
name: "multiple base64 private keys",
input: `
RSA: ` + validBase64RSA + `
EC: ` + validBase64EC,
want: []string{validBase64RSA, validBase64EC},
},
{
name: "deduplication - repeated key",
input: `PRIVATE_KEY=` + validBase64RSA + ` and PRIVATE_KEY=` + validBase64RSA,
want: []string{validBase64RSA},
},
{
name: "invalid - not base64",
input: `PRIVATE_KEY="not_valid_base64!!!"`,
want: nil,
},
{
name: "invalid - base64 but not private key",
input: base64.StdEncoding.EncodeToString([]byte("just some random text here")),
want: nil,
},
{
name: "invalid - too short base64",
input: `KEY="YWJjZGVm"`, // "abcdef" encoded - too short
want: nil,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
matchedDetectors := ahoCorasickCore.FindDetectorMatches([]byte(test.input))
if len(test.want) > 0 && len(matchedDetectors) == 0 {
t.Errorf("keywords %v not found in input", d.Keywords())
return
}

results, err := d.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)

if len(results) != len(test.want) {
t.Errorf("expected %d results, got %d", len(test.want), len(results))
return
}

actual := make(map[string]struct{}, len(results))
for _, r := range results {
if len(r.RawV2) > 0 {
actual[string(r.RawV2)] = struct{}{}
} else {
actual[string(r.Raw)] = struct{}{}
}
}

expected := make(map[string]struct{}, len(test.want))
for _, v := range test.want {
expected[v] = struct{}{}
}

if diff := cmp.Diff(expected, actual); diff != "" {
t.Errorf("%s diff: (-want +got)\n%s", test.name, diff)
}
})
}
}

func TestBase64PrivateKey_FromData(t *testing.T) {
rsaPrivateKey := `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA0Z6ca1yP6DGV/t5Z3i3h4z3Y8oHs5vZ0N2TQzL1d5dX5w3e
-----END RSA PRIVATE KEY-----`
validBase64 := base64.StdEncoding.EncodeToString([]byte(rsaPrivateKey))

tests := []struct {
name string
input string
wantResult []detectors.Result
}{
{
name: "base64 private key detected",
input: `PRIVATE_KEY="` + validBase64 + `"`,
wantResult: []detectors.Result{
{
DetectorType: detector_typepb.DetectorType_Base64PrivateKey,
Raw: []byte(validBase64),
Verified: false, // Private keys cannot be verified without context
},
},
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
s := Scanner{}
got, err := s.FromData(context.Background(), false, []byte(test.input))
require.NoError(t, err)

if diff := cmp.Diff(
test.wantResult,
got,
cmpopts.IgnoreFields(detectors.Result{}, "ExtraData", "SecretParts"),
cmpopts.IgnoreUnexported(detectors.Result{}),
); diff != "" {
t.Errorf("FromData() mismatch (-want +got):\n%s", diff)
}
})
}
}

func TestBase64PrivateKey_Type(t *testing.T) {
s := Scanner{}
require.Equal(t, detector_typepb.DetectorType_Base64PrivateKey, s.Type())
}

func TestBase64PrivateKey_Keywords(t *testing.T) {
s := Scanner{}
require.NotEmpty(t, s.Keywords())
require.Contains(t, s.Keywords(), "private")
require.Contains(t, s.Keywords(), "key")
}
2 changes: 2 additions & 0 deletions pkg/engine/defaults/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ import (
bannerbearv1 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bannerbear/v1"
bannerbearv2 "github.com/trufflesecurity/trufflehog/v3/pkg/detectors/bannerbear/v2"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/baremetrics"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/base64privatekey"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/beamer"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/beebole"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors/besttime"
Expand Down Expand Up @@ -972,6 +973,7 @@ func buildDetectorList() []detectors.Detector {
&bannerbearv1.Scanner{},
&bannerbearv2.Scanner{},
&baremetrics.Scanner{},
&base64privatekey.Scanner{},
&beamer.Scanner{},
&beebole.Scanner{},
// Besnappy appears to be abandoned. The domain has expired. API returns 200 OK for all secrets causing FPs
Expand Down
3 changes: 3 additions & 0 deletions pkg/pb/detector_typepb/detector_type.pb.go

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

1 change: 1 addition & 0 deletions proto/detector_type.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1054,4 +1054,5 @@ enum DetectorType {
GitLabOauth2 = 1050;
SpectralOps = 1051;
AWSAppSync = 1052;
Base64PrivateKey = 1056;
}