接口加密
This commit is contained in:
@@ -104,9 +104,10 @@ func (c *Client) doJSON(req *http.Request, out any) error {
|
|||||||
return fmt.Errorf("http %d: %s", resp.StatusCode, string(body))
|
return fmt.Errorf("http %d: %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
var envelope struct {
|
var envelope struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Result json.RawMessage `json:"result"`
|
Result json.RawMessage `json:"result"`
|
||||||
Message string `json:"message"`
|
ResultEncrypted bool `json:"result_encrypted"`
|
||||||
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, &envelope); err != nil {
|
if err := json.Unmarshal(body, &envelope); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -114,10 +115,22 @@ func (c *Client) doJSON(req *http.Request, out any) error {
|
|||||||
if envelope.Code != 0 {
|
if envelope.Code != 0 {
|
||||||
return fmt.Errorf("api code %d: %s", envelope.Code, envelope.Message)
|
return fmt.Errorf("api code %d: %s", envelope.Code, envelope.Message)
|
||||||
}
|
}
|
||||||
if out != nil && len(envelope.Result) > 0 {
|
if out == nil || len(envelope.Result) == 0 {
|
||||||
return json.Unmarshal(envelope.Result, out)
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
resultJSON := envelope.Result
|
||||||
|
if envelope.ResultEncrypted {
|
||||||
|
var cipherStr string
|
||||||
|
if err := json.Unmarshal(envelope.Result, &cipherStr); err != nil {
|
||||||
|
return fmt.Errorf("parse encrypted result: %w", err)
|
||||||
|
}
|
||||||
|
plain, err := DecryptBody(cipherStr, c.token)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("decrypt result: %w", err)
|
||||||
|
}
|
||||||
|
resultJSON = []byte(plain)
|
||||||
|
}
|
||||||
|
return json.Unmarshal(resultJSON, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchCreate 创建同步批次,返回 batch_id。
|
// BatchCreate 创建同步批次,返回 batch_id。
|
||||||
|
|||||||
122
internal/xkapi/crypto.go
Normal file
122
internal/xkapi/crypto.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package xkapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeriveTransitAESKey 从 HY_TRANSIT_API_TOKEN 派生 AES-128 密钥(与 PHP HyTransitCryptoService 一致)。
|
||||||
|
func DeriveTransitAESKey(token string) []byte {
|
||||||
|
sum := sha256.Sum256([]byte(token))
|
||||||
|
return sum[:16]
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptBody AES-128-ECB + PKCS7 加密为 Base64(与 PHP HyTransitCryptoService 一致)。
|
||||||
|
func EncryptBody(plain, token string) (string, error) {
|
||||||
|
return encryptTransitBody(plain, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptTransitBody(plain, token string) (string, error) {
|
||||||
|
key := DeriveTransitAESKey(token)
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
data := pkcs7Pad([]byte(plain), block.BlockSize())
|
||||||
|
out := make([]byte, len(data))
|
||||||
|
ecb := newECBEncrypter(block)
|
||||||
|
ecb.CryptBlocks(out, data)
|
||||||
|
return base64.StdEncoding.EncodeToString(out), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pkcs7Pad(data []byte, blockSize int) []byte {
|
||||||
|
pad := blockSize - (len(data) % blockSize)
|
||||||
|
padding := make([]byte, pad)
|
||||||
|
for i := range padding {
|
||||||
|
padding[i] = byte(pad)
|
||||||
|
}
|
||||||
|
return append(data, padding...)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ecbEncrypter struct {
|
||||||
|
b cipher.Block
|
||||||
|
blockSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newECBEncrypter(b cipher.Block) *ecbEncrypter {
|
||||||
|
return &ecbEncrypter{b: b, blockSize: b.BlockSize()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
|
||||||
|
if len(src)%x.blockSize != 0 {
|
||||||
|
panic("invalid padding")
|
||||||
|
}
|
||||||
|
for len(src) > 0 {
|
||||||
|
x.b.Encrypt(dst, src[:x.blockSize])
|
||||||
|
src = src[x.blockSize:]
|
||||||
|
dst = dst[x.blockSize:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptBody AES-128-ECB + PKCS7 解密 Base64 密文(与 PHP AesUtilService / hy.EncryptBody 对称)。
|
||||||
|
func DecryptBody(ciphertext, token string) (string, error) {
|
||||||
|
raw, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("base64 decode: %w", err)
|
||||||
|
}
|
||||||
|
key := DeriveTransitAESKey(token)
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(raw)%block.BlockSize() != 0 {
|
||||||
|
return "", fmt.Errorf("ciphertext length not multiple of block size")
|
||||||
|
}
|
||||||
|
out := make([]byte, len(raw))
|
||||||
|
ecb := newECBDecrypter(block)
|
||||||
|
ecb.CryptBlocks(out, raw)
|
||||||
|
plain, err := pkcs7Unpad(out, block.BlockSize())
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(plain), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pkcs7Unpad(data []byte, blockSize int) ([]byte, error) {
|
||||||
|
if len(data) == 0 || len(data)%blockSize != 0 {
|
||||||
|
return nil, fmt.Errorf("invalid padding size")
|
||||||
|
}
|
||||||
|
pad := int(data[len(data)-1])
|
||||||
|
if pad == 0 || pad > blockSize || pad > len(data) {
|
||||||
|
return nil, fmt.Errorf("invalid padding")
|
||||||
|
}
|
||||||
|
for i := 0; i < pad; i++ {
|
||||||
|
if data[len(data)-1-i] != byte(pad) {
|
||||||
|
return nil, fmt.Errorf("invalid padding bytes")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data[:len(data)-pad], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ecbDecrypter struct {
|
||||||
|
b cipher.Block
|
||||||
|
blockSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
func newECBDecrypter(b cipher.Block) *ecbDecrypter {
|
||||||
|
return &ecbDecrypter{b: b, blockSize: b.BlockSize()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
|
||||||
|
if len(src)%x.blockSize != 0 {
|
||||||
|
panic("invalid padding")
|
||||||
|
}
|
||||||
|
for len(src) > 0 {
|
||||||
|
x.b.Decrypt(dst, src[:x.blockSize])
|
||||||
|
src = src[x.blockSize:]
|
||||||
|
dst = dst[x.blockSize:]
|
||||||
|
}
|
||||||
|
}
|
||||||
49
internal/xkapi/crypto_test.go
Normal file
49
internal/xkapi/crypto_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package xkapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 固定向量须与 xk-api scripts/hy_transit_crypto_vector.php 输出一致。
|
||||||
|
const (
|
||||||
|
vectorTransitToken = "test-transit-api-token-for-golden-vector"
|
||||||
|
vectorPlainJSON = `{"batch_id":12,"organID":"o1","organName":"测试机构"}`
|
||||||
|
vectorDerivedKeyHex = "0887a3ab67abbbeda0901b562799c248"
|
||||||
|
vectorEncrypted = "V5D6xp9Txjj6lUB42NMgySnLw2ZC+nEVdYkbiufEtKEyYbMpNhuxeiAhnpg0xmw+ktGYuwnf9oeS8HbwBjJBBw=="
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeriveTransitAESKeyGolden(t *testing.T) {
|
||||||
|
got := hex.EncodeToString(DeriveTransitAESKey(vectorTransitToken))
|
||||||
|
if got != vectorDerivedKeyHex {
|
||||||
|
t.Fatalf("derived key %s != %s", got, vectorDerivedKeyHex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptGoldenMatchesVector(t *testing.T) {
|
||||||
|
enc, err := EncryptBody(vectorPlainJSON, vectorTransitToken)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if enc != vectorEncrypted {
|
||||||
|
t.Fatalf("encrypted %s != %s", enc, vectorEncrypted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecryptGoldenVector(t *testing.T) {
|
||||||
|
plain, err := DecryptBody(vectorEncrypted, vectorTransitToken)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if plain != vectorPlainJSON {
|
||||||
|
t.Fatalf("plain %q != %q", plain, vectorPlainJSON)
|
||||||
|
}
|
||||||
|
var decoded map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(plain), &decoded); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if decoded["batch_id"] != float64(12) {
|
||||||
|
t.Fatalf("batch_id %v", decoded["batch_id"])
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user