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 }