Files
cryptoHermes/internal/repo/webapi/coinglass/crypto.go
dela 1b61541ff5
Some checks failed
ci / build + vet + guard + race (push) Has been cancelled
feat: 接入 CoinGlass 清算热力图隔离链路
2026-05-25 16:57:22 +08:00

156 lines
4.7 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 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/msgparams 错等编码 bug
ErrAPIBusiness = errors.New("coinglass API business error")
)
// aesECBDecrypt 解密 AES-ECBstdlib 没有 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] 原 ASCIIcg.md §43-50no 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
}