mirror of
https://github.com/alexgetmancom/miband-bot.git
synced 2026-09-05 18:16:18 +03:00
290 lines
8.0 KiB
Python
290 lines
8.0 KiB
Python
"""小米云服务加密模块。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import base64
|
||
import hashlib
|
||
import json
|
||
import os
|
||
import struct
|
||
import time
|
||
from typing import Any
|
||
|
||
|
||
def _rc4_crypt(key: bytes, data: bytes, *, skip: int = 1024) -> bytes:
|
||
"""RC4 加密/解密(带前 N 字节跳过,防止密钥流弱点)。
|
||
|
||
Args:
|
||
key: RC4 密钥。
|
||
data: 待加密/解密的数据。
|
||
skip: 跳过前 N 字节的密钥流(默认 1024)。
|
||
|
||
Returns:
|
||
加密/解密后的字节数据。
|
||
"""
|
||
s = list(range(256))
|
||
j = 0
|
||
|
||
# KSA (Key-Scheduling Algorithm)
|
||
for i in range(256):
|
||
j = (j + s[i] + key[i % len(key)]) & 0xFF
|
||
s[i], s[j] = s[j], s[i]
|
||
|
||
# PRGA (Pseudo-Random Generation Algorithm)
|
||
i = 0
|
||
j = 0
|
||
|
||
# 跳过前 skip 字节密钥流
|
||
for _ in range(skip):
|
||
i = (i + 1) & 0xFF
|
||
j = (j + s[i]) & 0xFF
|
||
s[i], s[j] = s[j], s[i]
|
||
|
||
# 加密/解密
|
||
result = bytearray(len(data))
|
||
for idx in range(len(data)):
|
||
i = (i + 1) & 0xFF
|
||
j = (j + s[i]) & 0xFF
|
||
s[i], s[j] = s[j], s[i]
|
||
result[idx] = data[idx] ^ s[(s[i] + s[j]) & 0xFF]
|
||
|
||
return bytes(result)
|
||
|
||
|
||
def generate_nonce() -> str:
|
||
"""生成请求 nonce。
|
||
|
||
格式: base64(random_8_bytes + minutes_since_epoch_4bytes_BE)
|
||
|
||
Returns:
|
||
Base64 编码的 nonce 字符串。
|
||
"""
|
||
random_part = os.urandom(8)
|
||
minutes = int(time.time() / 60)
|
||
time_part = struct.pack(">I", minutes)
|
||
return base64.b64encode(random_part + time_part).decode()
|
||
|
||
|
||
def compute_signed_nonce(ssecurity: str, nonce: str) -> str:
|
||
"""计算签名 nonce(用于密钥派生)。
|
||
|
||
signed_nonce = base64(SHA256(b64decode(ssecurity) + b64decode(nonce)))
|
||
|
||
Args:
|
||
ssecurity: 登录时获取的 ssecurity(base64 编码)。
|
||
nonce: 请求 nonce。
|
||
|
||
Returns:
|
||
Base64 编码的 signed_nonce(同时作为 RC4 和 HMAC 的密钥)。
|
||
"""
|
||
hash_val = hashlib.sha256(base64.b64decode(ssecurity) + base64.b64decode(nonce)).digest()
|
||
return base64.b64encode(hash_val).decode()
|
||
|
||
|
||
# endregion
|
||
|
||
|
||
# region 签名生成
|
||
def _sha1_b64(message: str) -> str:
|
||
"""纯 SHA1 哈希 → Base64 编码。
|
||
|
||
App 使用 MessageDigest("SHA1") 而非 HMAC 来生成签名。
|
||
|
||
Args:
|
||
message: 待哈希的 UTF-8 字符串。
|
||
|
||
Returns:
|
||
Base64 编码的 SHA1 摘要(28 字符)。
|
||
"""
|
||
digest = hashlib.sha1(message.encode("utf-8")).digest()
|
||
return base64.b64encode(digest).decode()
|
||
|
||
|
||
def _build_sig_message(
|
||
method: str,
|
||
url_path: str,
|
||
params: dict[str, str],
|
||
signed_nonce: str,
|
||
) -> str:
|
||
"""构建签名消息字符串(z94.b 格式)。
|
||
|
||
格式: METHOD&/path&k1=v1&k2=v2&...&signedNonce_b64
|
||
- method 大写
|
||
- path 带前导 /
|
||
- params 按 key 字典序(TreeMap)排序
|
||
- 最后追加 signedNonce 的 base64 字符串
|
||
|
||
Args:
|
||
method: HTTP 方法。
|
||
url_path: URL 路径(须含前导 /)。
|
||
params: 参数字典(已排除空 key/value)。
|
||
signed_nonce: Base64 编码的 signed_nonce。
|
||
|
||
Returns:
|
||
用 & 连接的签名消息。
|
||
"""
|
||
parts: list[str] = [method.upper()]
|
||
if not url_path.startswith("/"):
|
||
url_path = "/" + url_path
|
||
parts.append(url_path)
|
||
for k in sorted(params.keys()):
|
||
parts.append(f"{k}={params[k]}")
|
||
parts.append(signed_nonce)
|
||
return "&".join(parts)
|
||
|
||
|
||
def _rc4_stream_encrypt_values(
|
||
key_bytes: bytes,
|
||
sorted_entries: list[tuple[str, str]],
|
||
) -> dict[str, str]:
|
||
"""用连续 RC4 流加密多个值。
|
||
|
||
模拟 App 中 d8k 的行为:构造时 drop 1024 字节,
|
||
然后对 TreeMap 中每个 entry 的 value 按排序顺序
|
||
依次加密,共用同一个 RC4 密钥流。
|
||
|
||
Args:
|
||
key_bytes: RC4 密钥(signed_nonce 的原始字节)。
|
||
sorted_entries: 按 key 排序的 (key, value) 对列表。
|
||
|
||
Returns:
|
||
{key: base64(encrypted_value)} 字典。
|
||
"""
|
||
# 将所有 value 拼接,一次性过 RC4 流
|
||
all_bytes = b"".join(v.encode("utf-8") for _, v in sorted_entries)
|
||
encrypted_all = _rc4_crypt(key_bytes, all_bytes, skip=1024)
|
||
|
||
result: dict[str, str] = {}
|
||
pos = 0
|
||
for k, v in sorted_entries:
|
||
vlen = len(v.encode("utf-8"))
|
||
result[k] = base64.b64encode(encrypted_all[pos : pos + vlen]).decode()
|
||
pos += vlen
|
||
return result
|
||
|
||
|
||
# endregion
|
||
|
||
|
||
# region 数据加密/解密
|
||
def encrypt_data(signed_nonce: str, plaintext: str) -> str:
|
||
"""用 RC4 加密数据。
|
||
|
||
Args:
|
||
signed_nonce: 密钥(base64 编码的 SHA256 哈希)。
|
||
plaintext: 明文 JSON 字符串。
|
||
|
||
Returns:
|
||
Base64 编码的密文。
|
||
"""
|
||
key = base64.b64decode(signed_nonce)
|
||
encrypted = _rc4_crypt(key, plaintext.encode("utf-8"))
|
||
return base64.b64encode(encrypted).decode()
|
||
|
||
|
||
def decrypt_data(signed_nonce: str, ciphertext_b64: str) -> str:
|
||
"""用 RC4 解密数据。
|
||
|
||
Args:
|
||
signed_nonce: 密钥(base64 编码的 SHA256 哈希)。
|
||
ciphertext_b64: Base64 编码的密文。
|
||
|
||
Returns:
|
||
解密后的明文字符串。
|
||
"""
|
||
key = base64.b64decode(signed_nonce)
|
||
decrypted = _rc4_crypt(key, base64.b64decode(ciphertext_b64))
|
||
return decrypted.decode("utf-8")
|
||
|
||
|
||
# endregion
|
||
|
||
|
||
# region 封装:构建加密请求参数
|
||
def build_encrypted_params(
|
||
method: str,
|
||
url_path: str,
|
||
ssecurity: str,
|
||
params: dict[str, Any] | None = None,
|
||
) -> dict[str, str]:
|
||
"""构建完整的加密请求参数。
|
||
|
||
流程(对应 App 中 ua4.c 方法):
|
||
1. 计算 signed_nonce = base64(SHA256(ssecurity + nonce))
|
||
2. 构建原始参数 TreeMap(排除空 key/value)
|
||
3. rc4_hash__ = SHA1(METHOD&/path&k=v&...&signedNonce) → base64
|
||
4. 将 rc4_hash__ 加入 TreeMap
|
||
5. 用连续 RC4 流加密所有 TreeMap 值(按 key 排序,drop 1024)
|
||
6. signature = SHA1(METHOD&/path&k=enc_v&...&signedNonce) → base64
|
||
7. 返回 {加密后各参数, signature, _nonce}
|
||
|
||
Args:
|
||
method: HTTP 方法。
|
||
url_path: API 路径(如 /app/v1/relatives/get_relative_list)。
|
||
ssecurity: 登录时获取的 ssecurity。
|
||
params: 要发送的参数字典(将被 JSON 序列化后加密)。
|
||
|
||
Returns:
|
||
包含 data, signature, rc4_hash__, _nonce 的参数字典。
|
||
"""
|
||
nonce = generate_nonce()
|
||
snonce = compute_signed_nonce(ssecurity, nonce)
|
||
snonce_bytes = base64.b64decode(snonce)
|
||
|
||
# Step 1: 构建原始参数 TreeMap(排除空 key/value)
|
||
raw_tree: dict[str, str] = {}
|
||
if params:
|
||
plaintext = json.dumps(params, separators=(",", ":"), ensure_ascii=False)
|
||
raw_tree["data"] = plaintext
|
||
|
||
# Step 2: 计算 rc4_hash__(基于原始参数)
|
||
rc4_msg = _build_sig_message(method, url_path, raw_tree, snonce)
|
||
rc4_hash_raw = _sha1_b64(rc4_msg)
|
||
|
||
# Step 3: 将 rc4_hash__ 插入 TreeMap
|
||
raw_tree["rc4_hash__"] = rc4_hash_raw
|
||
|
||
# Step 4: 用连续 RC4 流加密所有值
|
||
sorted_entries = sorted(raw_tree.items())
|
||
encrypted_values = _rc4_stream_encrypt_values(snonce_bytes, sorted_entries)
|
||
|
||
# Step 5: 构建加密后参数 TreeMap,计算 signature
|
||
sig_msg = _build_sig_message(method, url_path, encrypted_values, snonce)
|
||
signature = _sha1_b64(sig_msg)
|
||
|
||
# Step 6: 组装最终结果
|
||
result: dict[str, str] = {}
|
||
for k, v in encrypted_values.items():
|
||
result[k] = v
|
||
result["signature"] = signature
|
||
result["_nonce"] = nonce
|
||
return result
|
||
|
||
|
||
def decrypt_response(
|
||
ssecurity: str,
|
||
nonce: str,
|
||
ciphertext_b64: str,
|
||
) -> Any:
|
||
"""解密 API 响应。
|
||
|
||
Args:
|
||
ssecurity: 登录时获取的 ssecurity。
|
||
nonce: 请求时使用的 nonce。
|
||
ciphertext_b64: Base64 编码的响应密文。
|
||
|
||
Returns:
|
||
解密后的 JSON 对象。
|
||
"""
|
||
snonce = compute_signed_nonce(ssecurity, nonce)
|
||
plaintext = decrypt_data(snonce, ciphertext_b64)
|
||
|
||
try:
|
||
return json.loads(plaintext)
|
||
except json.JSONDecodeError:
|
||
# 可能不是 JSON,返回原始字符串
|
||
return plaintext
|
||
|
||
|
||
# endregion
|