181 lines
4.9 KiB
Go
181 lines
4.9 KiB
Go
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
|
|
}
|