// 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-сущности (<, &, ' и т.п.). 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 }