156 lines
4.7 KiB
Go
156 lines
4.7 KiB
Go
package coinglass
|
||
|
||
import (
|
||
"bytes"
|
||
"compress/gzip"
|
||
"crypto/aes"
|
||
"encoding/base64"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
)
|
||
|
||
var (
|
||
// ErrFakeSuccess — HTTP 200 + success=true + data 空。spec §4.6 / §11。
|
||
ErrFakeSuccess = errors.New("coinglass fake-success (TLS fingerprint suspected)")
|
||
|
||
// ErrUnknownV — dispatcher 见到未知 v 分支(bundle 可能升级)。spec §4.3。
|
||
ErrUnknownV = errors.New("coinglass unknown v branch")
|
||
|
||
// ErrDecryptFail — 双阶段解密任一步失败(base64 / aes / pkcs7 / gzip / utf8)。
|
||
ErrDecryptFail = errors.New("coinglass decrypt pipeline failed")
|
||
|
||
// ErrAPIBusiness — success=false 且有明确 code/msg(params 错等编码 bug)。
|
||
ErrAPIBusiness = errors.New("coinglass API business error")
|
||
)
|
||
|
||
// aesECBDecrypt 解密 AES-ECB(stdlib 没有 ECB 模式,按 block 循环 Decrypt)。
|
||
// 调用方保证 src 长度是 block size 的整数倍。
|
||
func aesECBDecrypt(key, src []byte) ([]byte, error) {
|
||
block, err := aes.NewCipher(key)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("%w: aes new cipher: %v", ErrDecryptFail, err)
|
||
}
|
||
bs := block.BlockSize()
|
||
if len(src) == 0 || len(src)%bs != 0 {
|
||
return nil, fmt.Errorf("%w: ciphertext len %d not multiple of block %d", ErrDecryptFail, len(src), bs)
|
||
}
|
||
dst := make([]byte, len(src))
|
||
for i := 0; i < len(src); i += bs {
|
||
block.Decrypt(dst[i:i+bs], src[i:i+bs])
|
||
}
|
||
return dst, nil
|
||
}
|
||
|
||
// aesECBEncrypt 加密 AES-ECB(仅 data= 签名用,spec §4.5)。
|
||
func aesECBEncrypt(key, src []byte) ([]byte, error) {
|
||
block, err := aes.NewCipher(key)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("%w: aes new cipher: %v", ErrDecryptFail, err)
|
||
}
|
||
bs := block.BlockSize()
|
||
if len(src)%bs != 0 {
|
||
return nil, fmt.Errorf("%w: plaintext len %d not multiple of block %d", ErrDecryptFail, len(src), bs)
|
||
}
|
||
dst := make([]byte, len(src))
|
||
for i := 0; i < len(src); i += bs {
|
||
block.Encrypt(dst[i:i+bs], src[i:i+bs])
|
||
}
|
||
return dst, nil
|
||
}
|
||
|
||
// pkcs7Pad / pkcs7Unpad — RFC 5652 §6.3。
|
||
func pkcs7Pad(b []byte, blockSize int) []byte {
|
||
pad := blockSize - len(b)%blockSize
|
||
out := make([]byte, len(b)+pad)
|
||
copy(out, b)
|
||
for i := len(b); i < len(out); i++ {
|
||
out[i] = byte(pad)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func pkcs7Unpad(b []byte, blockSize int) ([]byte, error) {
|
||
if len(b) == 0 || len(b)%blockSize != 0 {
|
||
return nil, fmt.Errorf("%w: pkcs7 unpad bad len %d", ErrDecryptFail, len(b))
|
||
}
|
||
pad := int(b[len(b)-1])
|
||
if pad == 0 || pad > blockSize {
|
||
return nil, fmt.Errorf("%w: pkcs7 unpad bad value %d", ErrDecryptFail, pad)
|
||
}
|
||
for i := len(b) - pad; i < len(b); i++ {
|
||
if int(b[i]) != pad {
|
||
return nil, fmt.Errorf("%w: pkcs7 unpad inconsistent", ErrDecryptFail)
|
||
}
|
||
}
|
||
return b[:len(b)-pad], nil
|
||
}
|
||
|
||
// decryptBlob — V2 通用管线:base64dec → AES-ECB → unpad → gunzip → strip outer quotes。
|
||
func decryptBlob(ciphertextB64 string, key []byte) (string, error) {
|
||
raw, err := base64.StdEncoding.DecodeString(ciphertextB64)
|
||
if err != nil {
|
||
return "", fmt.Errorf("%w: b64dec: %v", ErrDecryptFail, err)
|
||
}
|
||
decrypted, err := aesECBDecrypt(key, raw)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
unpadded, err := pkcs7Unpad(decrypted, aes.BlockSize)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
gz, err := gzip.NewReader(bytes.NewReader(unpadded))
|
||
if err != nil {
|
||
return "", fmt.Errorf("%w: gzip header: %v", ErrDecryptFail, err)
|
||
}
|
||
defer gz.Close()
|
||
plain, err := io.ReadAll(gz)
|
||
if err != nil {
|
||
return "", fmt.Errorf("%w: gunzip: %v", ErrDecryptFail, err)
|
||
}
|
||
text := string(plain)
|
||
if len(text) >= 2 && text[0] == '"' && text[len(text)-1] == '"' {
|
||
text = text[1 : len(text)-1]
|
||
}
|
||
return text, nil
|
||
}
|
||
|
||
// userKeyFromSeed — Stage 1 key 派生:base64(seed)[:16](cg.md L38)。
|
||
func userKeyFromSeed(seed string) []byte {
|
||
enc := base64.StdEncoding.EncodeToString([]byte(seed))
|
||
if len(enc) < 16 {
|
||
// 不应发生(任何非空 seed base64 后都 ≥ 4 chars,
|
||
// 实际 seed 至少 16 字节 → enc 至少 24 chars);保险兜底。
|
||
return []byte(enc)
|
||
}
|
||
return []byte(enc[:16])
|
||
}
|
||
|
||
// bodyKeyFromToken — Stage 2 key 派生:token[:16] 原 ASCII(cg.md §43-50:no second base64)。
|
||
func bodyKeyFromToken(token string) []byte {
|
||
if len(token) < 16 {
|
||
return []byte(token)
|
||
}
|
||
return []byte(token[:16])
|
||
}
|
||
|
||
// DecryptUniversal 跑完整双阶段管线(spec §4.4)。
|
||
//
|
||
// seed 由 dispatcher.resolveSeed(v, ...) 给出
|
||
// userHeaderB64 = response.headers["user"]
|
||
// bodyDataB64 = body.data (envelope 外层,未解密的 ciphertext)
|
||
//
|
||
// 返回 plaintext JSON 字节流,可直接喂给 mapper。
|
||
func DecryptUniversal(seed, userHeaderB64, bodyDataB64 string) ([]byte, error) {
|
||
token, err := decryptBlob(userHeaderB64, userKeyFromSeed(seed))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
plain, err := decryptBlob(bodyDataB64, bodyKeyFromToken(token))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
return []byte(plain), nil
|
||
}
|