129 lines
3.7 KiB
Go
129 lines
3.7 KiB
Go
package coinglass
|
||
|
||
import (
|
||
"bytes"
|
||
"compress/gzip"
|
||
"crypto/aes"
|
||
"encoding/base64"
|
||
"errors"
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
// helper:把 plaintext 用 V2 通用管线反向加密,产出可喂给 decryptBlob 的 base64 ciphertext。
|
||
// 形态:plaintext → '"plaintext"' → gzip → pkcs7 pad → AES-ECB → base64
|
||
func encryptForTest(t *testing.T, key []byte, plaintext string) string {
|
||
t.Helper()
|
||
|
||
// CoinGlass 服务器端会在 plaintext 外加引号(decryptBlob 反向 strip);
|
||
// 测试也照样加引号 → 验证 strip 逻辑。
|
||
quoted := []byte(`"` + plaintext + `"`)
|
||
|
||
var buf bytes.Buffer
|
||
gz := gzip.NewWriter(&buf)
|
||
_, err := gz.Write(quoted)
|
||
require.NoError(t, err)
|
||
require.NoError(t, gz.Close())
|
||
|
||
padded := pkcs7Pad(buf.Bytes(), aes.BlockSize)
|
||
enc, err := aesECBEncrypt(key, padded)
|
||
require.NoError(t, err)
|
||
return base64.StdEncoding.EncodeToString(enc)
|
||
}
|
||
|
||
func TestDecryptBlob_RoundTrip(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
key []byte
|
||
text string
|
||
}{
|
||
{"16-byte key (AES-128)", []byte("0123456789abcdef"), `{"hello":"world"}`},
|
||
{"32-byte key (AES-256)", []byte("1f68efd73f8d4921acc0dead41dd39bc"), `{"foo":42,"bar":"baz"}`},
|
||
{"empty json object", []byte("0123456789abcdef"), `{}`},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
ct := encryptForTest(t, tc.key, tc.text)
|
||
got, err := decryptBlob(ct, tc.key)
|
||
require.NoError(t, err)
|
||
require.Equal(t, tc.text, got)
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestDecryptBlob_BadCiphertext(t *testing.T) {
|
||
key := []byte("0123456789abcdef")
|
||
|
||
_, err := decryptBlob("!!!not_base64!!!", key)
|
||
require.Error(t, err)
|
||
require.ErrorIs(t, err, ErrDecryptFail)
|
||
|
||
short := base64.StdEncoding.EncodeToString([]byte("toosmall"))
|
||
_, err = decryptBlob(short, key)
|
||
require.ErrorIs(t, err, ErrDecryptFail)
|
||
}
|
||
|
||
func TestPKCS7_PadUnpadRoundTrip(t *testing.T) {
|
||
cases := [][]byte{
|
||
[]byte(""),
|
||
[]byte("a"),
|
||
[]byte("0123456789abcde"), // 15 → pad 1
|
||
[]byte("0123456789abcdef"), // 16 → pad 16 (full block)
|
||
[]byte("0123456789abcdef0"),
|
||
}
|
||
for _, in := range cases {
|
||
padded := pkcs7Pad(in, 16)
|
||
require.Zero(t, len(padded)%16)
|
||
out, err := pkcs7Unpad(padded, 16)
|
||
require.NoError(t, err)
|
||
require.Equal(t, in, out)
|
||
}
|
||
}
|
||
|
||
func TestPKCS7_UnpadRejectsBadInput(t *testing.T) {
|
||
bad := []byte("0123456789abcdef") // pad byte = 'f' (0x66 = 102 > 16)
|
||
_, err := pkcs7Unpad(bad, 16)
|
||
require.Error(t, err)
|
||
|
||
_, err = pkcs7Unpad([]byte{}, 16)
|
||
require.Error(t, err)
|
||
}
|
||
|
||
func TestUserKeyFromSeed_TruncatesTo16(t *testing.T) {
|
||
seed := "863f08689c97435b" // v=77 constant from bundle
|
||
got := userKeyFromSeed(seed)
|
||
require.Len(t, got, 16)
|
||
// base64("863f08689c97435b") = "ODYzZjA4Njg5Yzk3NDM1Yg==" → first 16 = "ODYzZjA4Njg5Yzk3"
|
||
require.Equal(t, "ODYzZjA4Njg5Yzk3", string(got))
|
||
}
|
||
|
||
func TestBodyKeyFromToken_RawASCIIFirst16(t *testing.T) {
|
||
token := "dfa89d5d00cf4000extra_chars"
|
||
got := bodyKeyFromToken(token)
|
||
require.Len(t, got, 16)
|
||
require.Equal(t, "dfa89d5d00cf4000", string(got))
|
||
}
|
||
|
||
func TestDecryptUniversal_TwoStage(t *testing.T) {
|
||
seed := "863f08689c97435b" // v=77 bundle constant
|
||
userKey := userKeyFromSeed(seed)
|
||
token := "dfa89d5d00cf4000" // exactly 16 chars (matches cg.md L79 example)
|
||
bodyKey := []byte(token)
|
||
|
||
plaintext := `{"success":true,"code":"0","msg":"","data":{"priceMin":110000.0}}`
|
||
|
||
userHeaderB64 := encryptForTest(t, userKey, token)
|
||
bodyDataB64 := encryptForTest(t, bodyKey, plaintext)
|
||
|
||
got, err := DecryptUniversal(seed, userHeaderB64, bodyDataB64)
|
||
require.NoError(t, err)
|
||
require.JSONEq(t, plaintext, string(got))
|
||
}
|
||
|
||
func TestDecryptUniversal_PropagatesDecryptFail(t *testing.T) {
|
||
_, err := DecryptUniversal("seed", "!!!", "!!!")
|
||
require.Error(t, err)
|
||
require.True(t, errors.Is(err, ErrDecryptFail))
|
||
}
|