feat: add Go API library
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user