238 lines
5.5 KiB
Go
238 lines
5.5 KiB
Go
// Package transforms exposes chainable transform primitives. Each
|
|
// function takes an input []byte and options, and returns an output
|
|
// or an error. They are pure and do not mutate input.
|
|
package transforms
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/flate"
|
|
"compress/gzip"
|
|
"compress/zlib"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"net/url"
|
|
)
|
|
|
|
const MaxInflateOutput = 64 << 20
|
|
|
|
// HexEncode returns ASCII hex of src.
|
|
func HexEncode(src []byte) ([]byte, error) {
|
|
return []byte(hex.EncodeToString(src)), nil
|
|
}
|
|
|
|
// HexDecode parses ASCII hex into bytes. Whitespace is ignored.
|
|
func HexDecode(src []byte) ([]byte, error) {
|
|
stripped := stripHex(src)
|
|
out := make([]byte, len(stripped)/2)
|
|
_, err := hex.Decode(out, stripped)
|
|
return out, err
|
|
}
|
|
|
|
func stripHex(s []byte) []byte {
|
|
out := make([]byte, 0, len(s))
|
|
for _, b := range s {
|
|
if b == ' ' || b == '\n' || b == '\r' || b == '\t' || b == ',' || b == ':' {
|
|
continue
|
|
}
|
|
if b == '0' && len(out) == 0 {
|
|
continue
|
|
}
|
|
out = append(out, b)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Base64Encode returns the standard Base64 of src.
|
|
func Base64Encode(src []byte) ([]byte, error) {
|
|
return []byte(base64.StdEncoding.EncodeToString(src)), nil
|
|
}
|
|
|
|
// Base64Decode accepts standard or URL-safe Base64, with or without padding.
|
|
func Base64Decode(src []byte) ([]byte, error) {
|
|
s := string(bytes.TrimSpace(src))
|
|
attempts := []*base64.Encoding{
|
|
base64.StdEncoding,
|
|
base64.URLEncoding,
|
|
base64.RawStdEncoding,
|
|
base64.RawURLEncoding,
|
|
}
|
|
for _, enc := range attempts {
|
|
if b, err := enc.DecodeString(s); err == nil {
|
|
return b, nil
|
|
}
|
|
}
|
|
return nil, errors.New("not valid base64")
|
|
}
|
|
|
|
// URLEncode applies percent-encoding.
|
|
func URLEncode(src []byte) ([]byte, error) {
|
|
return []byte(url.QueryEscape(string(src))), nil
|
|
}
|
|
|
|
// URLDecode reverses percent-encoding.
|
|
func URLDecode(src []byte) ([]byte, error) {
|
|
v, err := url.QueryUnescape(string(src))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []byte(v), nil
|
|
}
|
|
|
|
// XOR returns src^key, cycling key.
|
|
func XOR(src, key []byte) ([]byte, error) {
|
|
if len(key) == 0 {
|
|
return nil, errors.New("empty key")
|
|
}
|
|
out := make([]byte, len(src))
|
|
for i := range src {
|
|
out[i] = src[i] ^ key[i%len(key)]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// InflateGzip, InflateZlib, InflateRaw attempt to decompress.
|
|
func InflateGzip(src []byte) ([]byte, error) {
|
|
r, err := gzip.NewReader(bytes.NewReader(src))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer r.Close()
|
|
return readLimitedInflate(r)
|
|
}
|
|
|
|
func InflateZlib(src []byte) ([]byte, error) {
|
|
r, err := zlib.NewReader(bytes.NewReader(src))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer r.Close()
|
|
return readLimitedInflate(r)
|
|
}
|
|
|
|
func InflateRaw(src []byte) ([]byte, error) {
|
|
r := flate.NewReader(bytes.NewReader(src))
|
|
defer r.Close()
|
|
return readLimitedInflate(r)
|
|
}
|
|
|
|
func readLimitedInflate(r io.Reader) ([]byte, error) {
|
|
lr := io.LimitReader(r, MaxInflateOutput+1)
|
|
out, err := io.ReadAll(lr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(out) > MaxInflateOutput {
|
|
return nil, errors.New("decompressed output exceeds 64 MiB limit")
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// TryInflate attempts gzip, zlib, and raw deflate in order.
|
|
func TryInflate(src []byte) (out []byte, kind string, err error) {
|
|
if b, e := InflateGzip(src); e == nil {
|
|
return b, "gzip", nil
|
|
}
|
|
if b, e := InflateZlib(src); e == nil {
|
|
return b, "zlib", nil
|
|
}
|
|
if b, e := InflateRaw(src); e == nil {
|
|
return b, "raw", nil
|
|
}
|
|
return nil, "", errors.New("no decompression succeeded")
|
|
}
|
|
|
|
// Step is one entry in a transform pipeline.
|
|
type Step struct {
|
|
Op string // hex.encode, hex.decode, base64.encode, base64.decode,
|
|
// xor, inflate, url.encode, url.decode, caesar, rot13, atbash,
|
|
// vigenere.enc, vigenere.dec
|
|
Arg string // ASCII parameter (e.g. key for XOR/Vigenere, shift for Caesar)
|
|
}
|
|
|
|
// ApplyPipeline runs steps left-to-right. Intermediate results are
|
|
// returned for inspection.
|
|
func ApplyPipeline(input []byte, steps []Step) ([][]byte, error) {
|
|
results := make([][]byte, 0, len(steps)+1)
|
|
cur := append([]byte(nil), input...)
|
|
results = append(results, cur)
|
|
for _, s := range steps {
|
|
out, err := Apply(cur, s)
|
|
if err != nil {
|
|
return results, err
|
|
}
|
|
cur = out
|
|
results = append(results, cur)
|
|
}
|
|
return results, nil
|
|
}
|
|
|
|
// Apply one step.
|
|
func Apply(in []byte, s Step) ([]byte, error) {
|
|
switch s.Op {
|
|
case "hex.encode":
|
|
return HexEncode(in)
|
|
case "hex.decode":
|
|
return HexDecode(in)
|
|
case "base64.encode":
|
|
return Base64Encode(in)
|
|
case "base64.decode":
|
|
return Base64Decode(in)
|
|
case "url.encode":
|
|
return URLEncode(in)
|
|
case "url.decode":
|
|
return URLDecode(in)
|
|
case "xor":
|
|
key, err := parseKey(s.Arg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return XOR(in, key)
|
|
case "inflate":
|
|
switch s.Arg {
|
|
case "gzip":
|
|
return InflateGzip(in)
|
|
case "zlib":
|
|
return InflateZlib(in)
|
|
case "raw":
|
|
return InflateRaw(in)
|
|
default:
|
|
out, _, err := TryInflate(in)
|
|
return out, err
|
|
}
|
|
case "caesar":
|
|
n := 13
|
|
if s.Arg != "" {
|
|
var v int
|
|
for _, c := range s.Arg {
|
|
if c >= '0' && c <= '9' {
|
|
v = v*10 + int(c-'0')
|
|
}
|
|
}
|
|
n = v
|
|
}
|
|
return Caesar(in, n), nil
|
|
case "rot13":
|
|
return Caesar(in, 13), nil
|
|
case "atbash":
|
|
return Atbash(in), nil
|
|
case "vigenere.enc":
|
|
return Vigenere(in, []byte(s.Arg), true), nil
|
|
case "vigenere.dec":
|
|
return Vigenere(in, []byte(s.Arg), false), nil
|
|
}
|
|
return nil, errors.New("unknown op: " + s.Op)
|
|
}
|
|
|
|
// parseKey accepts either literal string (ASCII) or hex-prefixed "hex:..".
|
|
func parseKey(arg string) ([]byte, error) {
|
|
if len(arg) > 4 && arg[:4] == "hex:" {
|
|
return hex.DecodeString(arg[4:])
|
|
}
|
|
if arg == "" {
|
|
return nil, errors.New("empty key")
|
|
}
|
|
return []byte(arg), nil
|
|
}
|