mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-01 12:06:38 +08:00
5976a8d310
- rule/goja.go:纯 Go JS 运行时(goja),脚本编译缓存 + 超时中断(默认10s),
绑定集合对齐 legado evalJS(java/cookie/cache/source/book/result/baseUrl/
chapter/title/src/page/key)
- rule/bridge.go:java 对象函数名与 legado JsExtensions 一一对应(存量书源
硬编码这些名字),全部 Go 原生实现:
网络 ajax/ajaxAll/connect/get/post/head(经 MeBox HTTP 客户端,响应对象
带 body()/code()/url()/header());规则回调 getString/getStringList/
getElements/put/get(桥回当前解析器);编解码 base64/hex/md5(16/32)/
digest/HMac/encodeURI/timeFormat(SimpleDateFormat 转 Go 布局)
- 对称加密:createSymmetricCrypto + aes/des/3des 全家桶,transformation
三段式解析(AES/DES/3DES × CBC/ECB/CTR/OFB/CFB × PKCS5/7/Zero/NoPadding,
ECB 手工块循环),密钥长度自动补齐,decryptStr 自动识别 Base64/Hex
- get(url,headers) 与 get(key) 按 arity 分发(对齐 Kotlin 重载)
- 浏览器/文件/压缩包类函数明确抛不支持(服务端沙箱边界)
- URL 规则 JS:analyzeJs(@result 链式)、{{js}} 内嵌、option js/bodyJs
- execute 记录 Set-Cookie 进 cookie jar(java/cookie.getCookie 可读)
- 测试:编码/摘要/AES 向量/CBC+ECB 往返/ajax httptest/URL JS/沙箱边界,
全部通过;修复 java.get 双注册、测试缺 SetContent 与 md5Encode16 期望值
286 lines
7.2 KiB
Go
286 lines
7.2 KiB
Go
package rule
|
||
|
||
import (
|
||
"crypto/aes"
|
||
"crypto/cipher"
|
||
"crypto/des"
|
||
"encoding/hex"
|
||
"errors"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"github.com/dop251/goja"
|
||
)
|
||
|
||
// 对称加密:对应 legado JsEncodeUtils.createSymmetricCrypto(hutool SymmetricCrypto 语义)。
|
||
// transformation 形如 "AES/CBC/PKCS5Padding" / "DES/ECB/NoPadding" / "DESede/CBC/PKCS5Padding"。
|
||
|
||
type symmetricCipher struct {
|
||
block cipher.Block
|
||
mode string
|
||
padding string
|
||
iv []byte
|
||
}
|
||
|
||
func newSymmetricCipher(transformation, key, iv string) (*symmetricCipher, error) {
|
||
parts := strings.Split(strings.TrimSpace(transformation), "/")
|
||
algo := strings.ToUpper(strings.TrimSpace(parts[0]))
|
||
mode := "CBC"
|
||
padding := "PKCS5Padding"
|
||
if len(parts) > 1 && strings.TrimSpace(parts[1]) != "" {
|
||
mode = strings.ToUpper(strings.TrimSpace(parts[1]))
|
||
}
|
||
if len(parts) > 2 && strings.TrimSpace(parts[2]) != "" {
|
||
padding = strings.TrimSpace(parts[2])
|
||
}
|
||
|
||
keyBytes := normalizeKey([]byte(key), algo)
|
||
if len(keyBytes) == 0 {
|
||
return nil, errors.New("加密密钥为空")
|
||
}
|
||
var block cipher.Block
|
||
var err error
|
||
switch algo {
|
||
case "AES":
|
||
block, err = aes.NewCipher(keyBytes)
|
||
case "DES":
|
||
block, err = des.NewCipher(keyBytes)
|
||
case "DESEDE", "3DES", "TRIPLEDES":
|
||
block, err = des.NewTripleDESCipher(keyBytes)
|
||
default:
|
||
return nil, fmt.Errorf("不支持的加密算法: %s", algo)
|
||
}
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
ivBytes := []byte(iv)
|
||
if mode == "ECB" {
|
||
ivBytes = nil
|
||
}
|
||
return &symmetricCipher{block: block, mode: mode, padding: padding, iv: ivBytes}, nil
|
||
}
|
||
|
||
// normalizeKey 对齐 hutool 的密钥处理:按算法要求补齐/截断密钥长度。
|
||
func normalizeKey(key []byte, algo string) []byte {
|
||
var sizes []int
|
||
switch algo {
|
||
case "AES":
|
||
sizes = []int{16, 24, 32}
|
||
case "DES":
|
||
sizes = []int{8}
|
||
case "DESEDE", "3DES", "TRIPLEDES":
|
||
sizes = []int{24}
|
||
default:
|
||
return key
|
||
}
|
||
for _, n := range sizes {
|
||
if len(key) == n {
|
||
return key
|
||
}
|
||
}
|
||
for _, n := range sizes {
|
||
if len(key) < n {
|
||
out := make([]byte, n)
|
||
copy(out, key)
|
||
return out
|
||
}
|
||
}
|
||
return key[:sizes[len(sizes)-1]]
|
||
}
|
||
|
||
func (c *symmetricCipher) blockSize() int { return c.block.BlockSize() }
|
||
|
||
func (c *symmetricCipher) crypt(dst, src []byte, decrypt bool) error {
|
||
bs := c.blockSize()
|
||
if c.mode == "ECB" {
|
||
for i := 0; i < len(src); i += bs {
|
||
if decrypt {
|
||
c.block.Decrypt(dst[i:i+bs], src[i:i+bs])
|
||
} else {
|
||
c.block.Encrypt(dst[i:i+bs], src[i:i+bs])
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
iv := make([]byte, bs)
|
||
if len(c.iv) > 0 {
|
||
copy(iv, c.iv[:min(len(c.iv), bs)])
|
||
}
|
||
switch c.mode {
|
||
case "CBC":
|
||
if decrypt {
|
||
cipher.NewCBCDecrypter(c.block, iv).CryptBlocks(dst, src)
|
||
} else {
|
||
cipher.NewCBCEncrypter(c.block, iv).CryptBlocks(dst, src)
|
||
}
|
||
case "CTR":
|
||
cipher.NewCTR(c.block, iv).XORKeyStream(dst, src)
|
||
case "OFB":
|
||
cipher.NewOFB(c.block, iv).XORKeyStream(dst, src)
|
||
case "CFB", "CFB8":
|
||
stream := cipher.NewCFBEncrypter(c.block, iv)
|
||
if decrypt {
|
||
stream = cipher.NewCFBDecrypter(c.block, iv)
|
||
}
|
||
stream.XORKeyStream(dst, src)
|
||
default:
|
||
return fmt.Errorf("不支持的加密模式: %s", c.mode)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c *symmetricCipher) encrypt(data []byte) ([]byte, error) {
|
||
bs := c.blockSize()
|
||
data = applyPadding(data, bs, c.padding)
|
||
if len(data)%bs != 0 {
|
||
return nil, errors.New("加密数据长度未按块对齐")
|
||
}
|
||
out := make([]byte, len(data))
|
||
if err := c.crypt(out, data, false); err != nil {
|
||
return nil, err
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
func (c *symmetricCipher) decrypt(data []byte) ([]byte, error) {
|
||
bs := c.blockSize()
|
||
if len(data) == 0 || len(data)%bs != 0 {
|
||
return nil, errors.New("密文长度不是块大小的整数倍")
|
||
}
|
||
out := make([]byte, len(data))
|
||
if err := c.crypt(out, data, true); err != nil {
|
||
return nil, err
|
||
}
|
||
return stripPadding(out, c.padding), nil
|
||
}
|
||
|
||
// decryptAuto 对应 hutool decryptStr(String):输入自动识别 Base64 / Hex / 原文。
|
||
func (c *symmetricCipher) decryptAuto(s string) ([]byte, error) {
|
||
if b, err := base64DecodeBytes(s); err == nil && len(b) > 0 && len(b)%c.blockSize() == 0 {
|
||
return c.decrypt(b)
|
||
}
|
||
if b, err := hex.DecodeString(strings.TrimSpace(s)); err == nil && len(b) > 0 && len(b)%c.blockSize() == 0 {
|
||
return c.decrypt(b)
|
||
}
|
||
return c.decrypt([]byte(s))
|
||
}
|
||
|
||
func applyPadding(data []byte, blockSize int, padding string) []byte {
|
||
switch padding {
|
||
case "NoPadding":
|
||
return data
|
||
case "ZeroPadding":
|
||
n := (len(data)+blockSize-1)/blockSize*blockSize
|
||
if n == 0 {
|
||
n = blockSize
|
||
}
|
||
out := make([]byte, n)
|
||
copy(out, data)
|
||
return out
|
||
default: // PKCS5Padding / PKCS7Padding
|
||
pad := blockSize - len(data)%blockSize
|
||
out := make([]byte, len(data)+pad)
|
||
copy(out, data)
|
||
for i := len(data); i < len(out); i++ {
|
||
out[i] = byte(pad)
|
||
}
|
||
return out
|
||
}
|
||
}
|
||
|
||
func stripPadding(data []byte, padding string) []byte {
|
||
switch padding {
|
||
case "NoPadding":
|
||
return data
|
||
case "ZeroPadding":
|
||
for len(data) > 0 && data[len(data)-1] == 0 {
|
||
data = data[:len(data)-1]
|
||
}
|
||
return data
|
||
default:
|
||
if len(data) == 0 {
|
||
return data
|
||
}
|
||
pad := int(data[len(data)-1])
|
||
if pad <= 0 || pad > len(data) {
|
||
return data
|
||
}
|
||
for _, b := range data[len(data)-pad:] {
|
||
if int(b) != pad {
|
||
return data
|
||
}
|
||
}
|
||
return data[:len(data)-pad]
|
||
}
|
||
}
|
||
|
||
// newCipherObject 对应 hutool SymmetricCrypto 的 JS 方法面:
|
||
// encrypt(data) / encryptBase64(data) / encryptHex(data) / decrypt(data) /
|
||
// decryptStr(data) / decryptBase64(data) / decryptHex(data)。
|
||
func newCipherObject(vm *goja.Runtime, c *symmetricCipher) *goja.Object {
|
||
o := vm.NewObject()
|
||
set := func(k string, v any) {
|
||
if err := o.Set(k, v); err != nil {
|
||
panic(vm.ToValue(err.Error()))
|
||
}
|
||
}
|
||
set("encrypt", func(call goja.FunctionCall) goja.Value {
|
||
out, err := c.encrypt([]byte(stringArg(call, 0)))
|
||
if err != nil {
|
||
panic(vm.ToValue(err.Error()))
|
||
}
|
||
return vm.ToValue(vm.NewArrayBuffer(out))
|
||
})
|
||
set("encryptBase64", func(call goja.FunctionCall) goja.Value {
|
||
out, err := c.encrypt([]byte(stringArg(call, 0)))
|
||
if err != nil {
|
||
panic(vm.ToValue(err.Error()))
|
||
}
|
||
return vm.ToValue(base64StdEncodeBytes(out))
|
||
})
|
||
set("encryptHex", func(call goja.FunctionCall) goja.Value {
|
||
out, err := c.encrypt([]byte(stringArg(call, 0)))
|
||
if err != nil {
|
||
panic(vm.ToValue(err.Error()))
|
||
}
|
||
return vm.ToValue(hexEncodeBytes(out))
|
||
})
|
||
set("decrypt", func(call goja.FunctionCall) goja.Value {
|
||
out, err := c.decryptAuto(stringArg(call, 0))
|
||
if err != nil {
|
||
panic(vm.ToValue(err.Error()))
|
||
}
|
||
return vm.ToValue(vm.NewArrayBuffer(out))
|
||
})
|
||
set("decryptStr", func(call goja.FunctionCall) goja.Value {
|
||
out, err := c.decryptAuto(stringArg(call, 0))
|
||
if err != nil {
|
||
panic(vm.ToValue(err.Error()))
|
||
}
|
||
return vm.ToValue(string(out))
|
||
})
|
||
set("decryptBase64", func(call goja.FunctionCall) goja.Value {
|
||
b, err := base64DecodeBytes(stringArg(call, 0))
|
||
if err != nil {
|
||
panic(vm.ToValue(err.Error()))
|
||
}
|
||
out, err := c.decrypt(b)
|
||
if err != nil {
|
||
panic(vm.ToValue(err.Error()))
|
||
}
|
||
return vm.ToValue(string(out))
|
||
})
|
||
set("decryptHex", func(call goja.FunctionCall) goja.Value {
|
||
b, err := hexDecodeBytes(strings.TrimSpace(stringArg(call, 0)))
|
||
if err != nil {
|
||
panic(vm.ToValue(err.Error()))
|
||
}
|
||
out, err := c.decrypt(b)
|
||
if err != nil {
|
||
panic(vm.ToValue(err.Error()))
|
||
}
|
||
return vm.ToValue(string(out))
|
||
})
|
||
return o
|
||
}
|