burterm/internal/decoder/decoder.go
2026-09-14 10:55:07 +03:00

223 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package decoder реализует набор кодировок/декодировок (аналог вкладки
// Decoder в Burp): URL, Base64, HTML-entities, Hex и однонаправленный MD5,
// плюс SmartDecode — эвристическая цепочка автоматического декодирования,
// подбирающая, какая кодировка применена, и снимающая её слой за слоем.
package decoder
import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"html"
"net/url"
"strings"
)
// URLEncode кодирует строку как query-компонент URL (пробел → '+').
func URLEncode(s string) string {
return url.QueryEscape(s)
}
// URLDecode декодирует query-компонент URL.
func URLDecode(s string) (string, error) {
out, err := url.QueryUnescape(s)
if err != nil {
return "", fmt.Errorf("URL-декодирование: %w", err)
}
return out, nil
}
// Base64Encode кодирует в стандартный Base64 (с паддингом).
func Base64Encode(s string) string {
return base64StdEncode([]byte(s))
}
// Base64Decode декодирует Base64, принимая как стандартный, так и
// URL-safe алфавит и терпимо относясь к отсутствующему паддингу —
// на практике токены из веба встречаются в обоих вариантах.
func Base64Decode(s string) (string, error) {
b, err := base64FlexibleDecode(s)
if err != nil {
return "", fmt.Errorf("Base64-декодирование: %w", err)
}
return string(b), nil
}
// HTMLEncode экранирует HTML-спецсимволы (<, >, &, ", ').
func HTMLEncode(s string) string {
return html.EscapeString(s)
}
// HTMLDecode разворачивает HTML-сущности (&lt;, &amp;, &#39; и т.п.).
func HTMLDecode(s string) string {
return html.UnescapeString(s)
}
// HexEncode кодирует строку в шестнадцатеричное представление.
func HexEncode(s string) string {
return hex.EncodeToString([]byte(s))
}
// HexDecode декодирует шестнадцатеричную строку обратно в текст.
func HexDecode(s string) (string, error) {
b, err := hex.DecodeString(strings.TrimSpace(s))
if err != nil {
return "", fmt.Errorf("hex-декодирование: %w", err)
}
return string(b), nil
}
// base64StdEncode — тонкая обёртка над стандартной библиотекой,
// вынесенная в отдельную функцию ради симметрии с base64FlexibleDecode.
func base64StdEncode(b []byte) string {
return base64.StdEncoding.EncodeToString(b)
}
// base64FlexibleDecode перебирает стандартный и URL-safe алфавиты,
// с паддингом и без — то есть все четыре комбинации, которые реально
// встречаются в вебе (JWT, например, использует RawURLEncoding).
func base64FlexibleDecode(s string) ([]byte, error) {
encodings := []*base64.Encoding{
base64.StdEncoding,
base64.RawStdEncoding,
base64.URLEncoding,
base64.RawURLEncoding,
}
var lastErr error
for _, enc := range encodings {
if b, err := enc.DecodeString(s); err == nil {
return b, nil
} else {
lastErr = err
}
}
return nil, lastErr
}
// MD5Hash — однонаправленное хэширование, декодирования не существует.
func MD5Hash(s string) string {
sum := md5.Sum([]byte(s))
return hex.EncodeToString(sum[:])
}
// Step — один шаг цепочки SmartDecode: какая кодировка была снята
// и что получилось после этого шага.
type Step struct {
Operation string
Result string
}
// SmartDecode пытается определить, чем закодирована строка, и снимает
// кодировки слой за слоем, пока распознаётся хотя бы одна из известных
// (URL, Base64, Hex, HTML) или не достигнут предел итераций. Это
// эвристика, а не гарантированно верное определение — при неоднозначных
// входах (например, строка, которая одновременно валидный Base64 и
// валидный hex) приоритет фиксирован в порядке проверки ниже.
func SmartDecode(s string) []Step {
const maxSteps = 8
var steps []Step
cur := s
for i := 0; i < maxSteps; i++ {
op, next, ok := detectAndDecodeOne(cur)
if !ok || next == cur {
break
}
steps = append(steps, Step{Operation: op, Result: next})
cur = next
}
return steps
}
// detectAndDecodeOne проверяет кодировки по очереди и применяет первую
// подошедшую. Порядок проверки важен: URL-encoding и HTML-entities имеют
// узнаваемые маркеры (%XX, &...;) и проверяются первыми как самые
// однозначные; Base64 и Hex — по составу алфавита, где Hex строже
// (только 0-9a-f), поэтому его проверяем раньше Base64.
func detectAndDecodeOne(s string) (op, result string, ok bool) {
trimmed := strings.TrimSpace(s)
if strings.Contains(s, "%") && looksURLEncoded(s) {
if out, err := URLDecode(s); err == nil && out != s {
return "URL-decode", out, true
}
}
if strings.Contains(s, "&") && strings.Contains(s, ";") {
if out := HTMLDecode(s); out != s {
return "HTML-decode", out, true
}
}
if looksHex(trimmed) {
if out, err := HexDecode(trimmed); err == nil && isPrintable(out) {
return "Hex-decode", out, true
}
}
if looksBase64(trimmed) {
if out, err := Base64Decode(trimmed); err == nil && isPrintable(out) {
return "Base64-decode", out, true
}
}
return "", "", false
}
func looksURLEncoded(s string) bool {
for i := 0; i < len(s)-2; i++ {
if s[i] == '%' && isHexByte(s[i+1]) && isHexByte(s[i+2]) {
return true
}
}
return false
}
func isHexByte(c byte) bool {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}
func looksHex(s string) bool {
if s == "" || len(s)%2 != 0 {
return false
}
for i := 0; i < len(s); i++ {
if !isHexByte(s[i]) {
return false
}
}
return true
}
func looksBase64(s string) bool {
if s == "" || len(s)%4 != 0 {
return false
}
for i := 0; i < len(s); i++ {
c := s[i]
switch {
case c >= 'A' && c <= 'Z', c >= 'a' && c <= 'z', c >= '0' && c <= '9', c == '+', c == '/', c == '-', c == '_', c == '=':
default:
return false
}
}
return true
}
// isPrintable — грубый фильтр "похоже на текст, а не на мусор", чтобы
// SmartDecode не радостно декодировал случайные бинарные данные,
// формально прошедшие проверку алфавита Base64/Hex, но не являющиеся
// осмысленной строкой на выходе.
func isPrintable(s string) bool {
if s == "" {
return false
}
printable := 0
for _, r := range s {
if r == '\n' || r == '\r' || r == '\t' || (r >= 0x20 && r < 0x7f) {
printable++
}
}
return float64(printable)/float64(len([]rune(s))) > 0.85
}