Files
trufflehog/pkg/decoders/base64.go
2024-10-25 23:10:56 -07:00

184 lines
4.9 KiB
Go

package decoders
import (
"bytes"
"encoding/base64"
"sort"
"unicode"
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
)
type (
Base64 struct{}
)
var (
b64Charset = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/-_=")
b64EndChars = "+/-_="
// Given characters are mostly ASCII, we can use a simple array to map.
b64CharsetMapping [128]bool
)
func init() {
// Build an array of all the characters in the base64 charset.
for _, char := range b64Charset {
b64CharsetMapping[char] = true
}
}
func (d *Base64) Type() detectorspb.DecoderType {
return detectorspb.DecoderType_BASE64
}
func (d *Base64) FromChunk(chunk *sources.Chunk) *DecodableChunk {
decodableChunk := &DecodableChunk{Chunk: chunk, DecoderType: d.Type()}
encodedSubstrings := getSubstringsOfCharacterSet(chunk.Data, 20, b64CharsetMapping, b64EndChars)
decodedSubstrings := make(map[string][]byte)
for _, str := range encodedSubstrings {
dec, err := base64.StdEncoding.DecodeString(str)
if err == nil && len(dec) > 0 && isASCII(dec) {
decodedSubstrings[str] = dec
}
dec, err = base64.RawURLEncoding.DecodeString(str)
if err == nil && len(dec) > 0 && isASCII(dec) {
decodedSubstrings[str] = dec
}
}
if len(decodedSubstrings) > 0 {
// Build a position map and sort encoded strings by their position in the original data.
// This ensures we process the data in a single forward pass, handling each encoded string
// in order of appearance. By sorting first, we guarantee sequential processing and avoid
// potential backward scans through already processed data.
positionMap := make(map[string]int, len(encodedSubstrings))
remaining := chunk.Data
pos := 0
for _, encoded := range encodedSubstrings {
if idx := bytes.Index(remaining, []byte(encoded)); idx != -1 {
positionMap[encoded] = pos + idx
pos += idx
remaining = remaining[idx+len(encoded):]
}
}
var result bytes.Buffer
result.Grow(len(chunk.Data))
start := 0
// Sort encodedSubstrings by their positions for sequential processing.
sortedSubstrings := make([]string, 0, len(positionMap))
for encoded := range positionMap {
sortedSubstrings = append(sortedSubstrings, encoded)
}
sort.Slice(sortedSubstrings, func(i, j int) bool {
return positionMap[sortedSubstrings[i]] < positionMap[sortedSubstrings[j]]
})
// Process in sequential order.
for _, encoded := range sortedSubstrings {
if decoded, ok := decodedSubstrings[encoded]; ok {
pos := positionMap[encoded]
result.Write(chunk.Data[start:pos])
result.Write(decoded)
start = pos + len(encoded)
}
}
result.Write(chunk.Data[start:])
chunk.Data = result.Bytes()
return decodableChunk
}
return nil
}
func isASCII(b []byte) bool {
for i := 0; i < len(b); i++ {
if b[i] > unicode.MaxASCII {
return false
}
}
return true
}
// getSubstringsOfCharacterSet extracts base64-encoded substrings
// from byte data that meet the threshold.
func getSubstringsOfCharacterSet(data []byte, threshold int, charsetMapping [128]bool, endChars string) []string {
if len(data) == 0 {
return nil
}
// initialSubstringsCapacity is set to 16 as a practical optimization,
// balancing memory usage with the typical number of base64-encoded
// strings found in common input data. (this could be tuned further)
const initialSubstringsCapacity = 16
substrings := make([]string, 0, initialSubstringsCapacity)
count := 0
start := 0
for i := range data {
char := data[i]
isValid := char < 128 && charsetMapping[char]
if isValid {
if count == 0 {
start = i
}
count++
continue
}
if count > threshold {
substrings = appendB64Substring(data, start, count, substrings, endChars)
}
count = 0
}
if count > threshold {
substrings = appendB64Substring(data, start, count, substrings, endChars)
}
return substrings
}
// appendB64Substring processes and appends a base64 substring to the result slice.
// It handles both standard and URL-safe base64 encodings, preserving padding characters
// and special characters like underscores.
func appendB64Substring(data []byte, start, count int, substrings []string, endChars string) []string {
left := start
right := start + count - 1
// Trim left end chars except underscore.
for left < right && bytes.IndexByte([]byte(endChars), data[left]) != -1 && data[left] != '_' {
left++
}
// Trim right end chars except underscore and equals.
for right > left && bytes.IndexByte([]byte(endChars), data[right]) != -1 {
if data[right] == '=' || data[right] == '_' {
break
}
right--
}
// Find equals sign in the middle if present.
for i := left; i < right; i++ {
if data[i] == '=' {
left = i + 1
break
}
}
// Only append if we have valid data after trimming.
if right >= left {
substrings = append(substrings, string(data[left:right+1]))
}
return substrings
}