feat: add Go API library

This commit is contained in:
2026-07-18 22:40:40 +08:00
commit 4755655859
23 changed files with 5346 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
package mijia
import (
"bytes"
"compress/gzip"
"crypto/rand"
"crypto/rc4"
"crypto/sha1"
"crypto/sha256"
"encoding/base64"
"fmt"
"io"
"strings"
"time"
"unicode/utf8"
)
const (
rc4DropBytes = 1024
maxDecompressedResponseBytes = 16 << 20
)
type orderedParam struct {
Key string
Value string
}
type encryptedParams struct {
Data string
RC4Hash string
Signature string
Ssecurity string
Nonce string
}
func generateNonce() (string, error) {
randomBytes := make([]byte, 8)
if _, err := rand.Read(randomBytes); err != nil {
return "", fmt.Errorf("generate nonce: %w", err)
}
minute := uint64(time.Now().Unix() / 60)
minuteBytes := make([]byte, 0, 8)
for value := minute; value > 0; value >>= 8 {
minuteBytes = append(minuteBytes, byte(value))
}
if len(minuteBytes) == 0 {
minuteBytes = append(minuteBytes, 0)
}
for left, right := 0, len(minuteBytes)-1; left < right; left, right = left+1, right-1 {
minuteBytes[left], minuteBytes[right] = minuteBytes[right], minuteBytes[left]
}
nonce := append(randomBytes, minuteBytes...)
return base64.StdEncoding.EncodeToString(nonce), nil
}
func signedNonce(ssecurity, nonce string) (string, error) {
securityBytes, err := base64.StdEncoding.DecodeString(ssecurity)
if err != nil {
return "", fmt.Errorf("decode ssecurity: %w", err)
}
nonceBytes, err := base64.StdEncoding.DecodeString(nonce)
if err != nil {
return "", fmt.Errorf("decode nonce: %w", err)
}
digest := sha256.New()
_, _ = digest.Write(securityBytes)
_, _ = digest.Write(nonceBytes)
return base64.StdEncoding.EncodeToString(digest.Sum(nil)), nil
}
func encryptedSignature(uri, method string, params []orderedParam, nonce string) string {
parts := make([]string, 0, len(params)+3)
parts = append(parts, strings.ToUpper(method), uri)
for _, param := range params {
parts = append(parts, param.Key+"="+param.Value)
}
parts = append(parts, nonce)
digest := sha1.Sum([]byte(strings.Join(parts, "&")))
return base64.StdEncoding.EncodeToString(digest[:])
}
func generateEncryptedParams(uri, method, signedNonceValue, nonce, data, ssecurity string) (encryptedParams, error) {
plainParams := []orderedParam{{Key: "data", Value: data}}
rc4Hash := encryptedSignature(uri, method, plainParams, signedNonceValue)
encryptedData, err := encryptRC4(signedNonceValue, data)
if err != nil {
return encryptedParams{}, fmt.Errorf("encrypt data: %w", err)
}
encryptedRC4Hash, err := encryptRC4(signedNonceValue, rc4Hash)
if err != nil {
return encryptedParams{}, fmt.Errorf("encrypt rc4_hash__: %w", err)
}
encrypted := []orderedParam{
{Key: "data", Value: encryptedData},
{Key: "rc4_hash__", Value: encryptedRC4Hash},
}
return encryptedParams{
Data: encryptedData,
RC4Hash: encryptedRC4Hash,
Signature: encryptedSignature(uri, method, encrypted, signedNonceValue),
Ssecurity: ssecurity,
Nonce: nonce,
}, nil
}
func encryptRC4(password, payload string) (string, error) {
return encryptRC4Bytes(password, []byte(payload))
}
func encryptRC4Bytes(password string, payload []byte) (string, error) {
result, err := cryptRC4(password, payload)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(result), nil
}
func decryptRC4(password, payload string) ([]byte, error) {
ciphertext, err := base64.StdEncoding.DecodeString(payload)
if err != nil {
return nil, fmt.Errorf("decode RC4 payload: %w", err)
}
return cryptRC4(password, ciphertext)
}
func cryptRC4(password string, payload []byte) ([]byte, error) {
key, err := base64.StdEncoding.DecodeString(password)
if err != nil {
return nil, fmt.Errorf("decode RC4 key: %w", err)
}
cipher, err := rc4.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("create RC4 cipher: %w", err)
}
discard := make([]byte, rc4DropBytes)
cipher.XORKeyStream(discard, discard)
result := make([]byte, len(payload))
cipher.XORKeyStream(result, payload)
return result, nil
}
func decryptPayload(ssecurity, nonce, payload string) (string, error) {
signedNonceValue, err := signedNonce(ssecurity, nonce)
if err != nil {
return "", err
}
decrypted, err := decryptRC4(signedNonceValue, payload)
if err != nil {
return "", err
}
if utf8.Valid(decrypted) {
return string(decrypted), nil
}
reader, err := gzip.NewReader(bytes.NewReader(decrypted))
if err != nil {
return "", fmt.Errorf("open gzip response: %w", err)
}
decompressed, readErr := io.ReadAll(io.LimitReader(reader, maxDecompressedResponseBytes+1))
closeErr := reader.Close()
if readErr != nil {
return "", fmt.Errorf("decompress response: %w", readErr)
}
if closeErr != nil {
return "", fmt.Errorf("close gzip response: %w", closeErr)
}
if len(decompressed) > maxDecompressedResponseBytes {
return "", fmt.Errorf("decompressed response exceeds %d bytes", maxDecompressedResponseBytes)
}
if !utf8.Valid(decompressed) {
return "", fmt.Errorf("decrypted response is not valid UTF-8")
}
return string(decompressed), nil
}