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 }