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
+171
View File
@@ -0,0 +1,171 @@
package mijia
import (
"bytes"
"compress/gzip"
"encoding/base64"
"strings"
"testing"
"time"
)
const (
testSsecurity = "MDEyMzQ1Njc4OWFiY2RlZg=="
testNonce = "AAECAwQFBgcICQoL"
testSignedNonce = "16/CeTzC9IqVVbiZ01Hy/Qd8rtVo5ybLo+ph/Vvh52k="
)
func TestGenerateNonce(t *testing.T) {
before := time.Now().Unix() / 60
nonce, err := generateNonce()
if err != nil {
t.Fatalf("generateNonce() error = %v", err)
}
after := time.Now().Unix() / 60
decoded, err := base64.StdEncoding.DecodeString(nonce)
if err != nil {
t.Fatalf("generated nonce is not base64: %v", err)
}
if len(decoded) <= 8 {
t.Fatalf("decoded nonce length = %d, want more than 8", len(decoded))
}
minute := decodeBigEndian(decoded[8:])
if minute < before || minute > after {
t.Fatalf("nonce minute = %d, want between %d and %d", minute, before, after)
}
if len(decoded[8:]) > 1 && decoded[8] == 0 {
t.Fatal("nonce minute is not minimally encoded")
}
}
func TestSignedNonce(t *testing.T) {
got, err := signedNonce(testSsecurity, testNonce)
if err != nil {
t.Fatalf("signedNonce() error = %v", err)
}
if got != testSignedNonce {
t.Fatalf("signedNonce() = %q, want %q", got, testSignedNonce)
}
}
func TestRC4Drop1024(t *testing.T) {
ciphertext, err := encryptRC4(testSignedNonce, "hello world")
if err != nil {
t.Fatalf("encryptRC4() error = %v", err)
}
if ciphertext != "9ve6riTrkW1oJUE=" {
t.Fatalf("encryptRC4() = %q, want fixed ciphertext", ciphertext)
}
plaintext, err := decryptRC4(testSignedNonce, ciphertext)
if err != nil {
t.Fatalf("decryptRC4() error = %v", err)
}
if string(plaintext) != "hello world" {
t.Fatalf("decryptRC4() = %q, want %q", plaintext, "hello world")
}
}
func TestGenerateEncryptedParams(t *testing.T) {
params, err := generateEncryptedParams("/miotspec/prop/get", "post", testSignedNonce, testNonce, "{\"k\":\"v\"}", testSsecurity)
if err != nil {
t.Fatalf("generateEncryptedParams() error = %v", err)
}
want := encryptedParams{
Data: "5bC94HHpkCBn",
RC4Hash: "19WFsROvqTVVE0wemsEqz8GI7Ib8ZRiiLJS5hQ==",
Signature: "uRPqk6IHkFCAg+Skfo+5l/KbTpM=",
Ssecurity: testSsecurity,
Nonce: testNonce,
}
if params != want {
t.Fatalf("generateEncryptedParams() = %#v, want %#v", params, want)
}
firstSignature := encryptedSignature("/miotspec/prop/get", "post", []orderedParam{{Key: "data", Value: "{\"k\":\"v\"}"}}, testSignedNonce)
if firstSignature != "IGSsXdO7OZiyN58pywgUYELur6g=" {
t.Fatalf("first signature = %q, want fixed signature", firstSignature)
}
}
func TestDecryptPayload(t *testing.T) {
plaintext, err := decryptPayload(testSsecurity, testNonce, "9ve6riTrkW1oJUE=")
if err != nil {
t.Fatalf("decryptPayload() plain error = %v", err)
}
if plaintext != "hello world" {
t.Fatalf("decryptPayload() plain = %q, want %q", plaintext, "hello world")
}
var compressed bytes.Buffer
writer := gzip.NewWriter(&compressed)
if _, err := writer.Write([]byte("gzip response")); err != nil {
t.Fatalf("gzip write: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("gzip close: %v", err)
}
ciphertext, err := encryptRC4Bytes(testSignedNonce, compressed.Bytes())
if err != nil {
t.Fatalf("encrypt compressed response: %v", err)
}
decrypted, err := decryptPayload(testSsecurity, testNonce, ciphertext)
if err != nil {
t.Fatalf("decryptPayload() gzip error = %v", err)
}
if decrypted != "gzip response" {
t.Fatalf("decryptPayload() gzip = %q, want %q", decrypted, "gzip response")
}
}
func TestDecryptPayloadInvalidBase64(t *testing.T) {
_, err := decryptPayload(testSsecurity, testNonce, "not-base64")
if err == nil || !strings.Contains(err.Error(), "decode RC4 payload") {
t.Fatalf("decryptPayload() error = %v, want invalid base64 error", err)
}
}
func TestDecryptPayloadDamagedGzip(t *testing.T) {
ciphertext, err := encryptRC4Bytes(testSignedNonce, []byte{0x1f, 0x8b, 0xff})
if err != nil {
t.Fatalf("encrypt damaged gzip response: %v", err)
}
_, err = decryptPayload(testSsecurity, testNonce, ciphertext)
if err == nil || !strings.Contains(err.Error(), "open gzip response") {
t.Fatalf("decryptPayload() error = %v, want damaged gzip error", err)
}
}
func TestDecryptPayloadDecompressedLimit(t *testing.T) {
var compressed bytes.Buffer
writer := gzip.NewWriter(&compressed)
oversized := bytes.Repeat([]byte{0xff}, maxDecompressedResponseBytes+1)
if _, err := writer.Write(oversized); err != nil {
t.Fatalf("gzip write: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("gzip close: %v", err)
}
ciphertext, err := encryptRC4Bytes(testSignedNonce, compressed.Bytes())
if err != nil {
t.Fatalf("encrypt oversized gzip response: %v", err)
}
_, err = decryptPayload(testSsecurity, testNonce, ciphertext)
if err == nil || !strings.Contains(err.Error(), "decompressed response exceeds") {
t.Fatalf("decryptPayload() error = %v, want decompressed limit error", err)
}
}
func decodeBigEndian(value []byte) int64 {
var result int64
for _, current := range value {
result = result<<8 | int64(current)
}
return result
}