cookiecloud-decrypt 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cookiecloud_decrypt-0.1.0/PKG-INFO +145 -0
- cookiecloud_decrypt-0.1.0/README.md +120 -0
- cookiecloud_decrypt-0.1.0/cookiecloud_decrypt/__init__.py +61 -0
- cookiecloud_decrypt-0.1.0/cookiecloud_decrypt/decrypt.py +309 -0
- cookiecloud_decrypt-0.1.0/cookiecloud_decrypt/format.py +222 -0
- cookiecloud_decrypt-0.1.0/cookiecloud_decrypt/models.py +40 -0
- cookiecloud_decrypt-0.1.0/cookiecloud_decrypt.egg-info/PKG-INFO +145 -0
- cookiecloud_decrypt-0.1.0/cookiecloud_decrypt.egg-info/SOURCES.txt +13 -0
- cookiecloud_decrypt-0.1.0/cookiecloud_decrypt.egg-info/dependency_links.txt +1 -0
- cookiecloud_decrypt-0.1.0/cookiecloud_decrypt.egg-info/requires.txt +7 -0
- cookiecloud_decrypt-0.1.0/cookiecloud_decrypt.egg-info/top_level.txt +1 -0
- cookiecloud_decrypt-0.1.0/pyproject.toml +43 -0
- cookiecloud_decrypt-0.1.0/setup.cfg +4 -0
- cookiecloud_decrypt-0.1.0/tests/test_decrypt.py +211 -0
- cookiecloud_decrypt-0.1.0/tests/test_format.py +260 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cookiecloud_decrypt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CookieCloud data decryption SDK — supports legacy and aes-128-cbc-fixed modes
|
|
5
|
+
Author-email: cky <cky@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/YOUR_USERNAME/cookiecloud_decrypt
|
|
8
|
+
Project-URL: Repository, https://github.com/YOUR_USERNAME/cookiecloud_decrypt
|
|
9
|
+
Keywords: cookiecloud,cookie,decrypt,web-scraping
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
17
|
+
Requires-Python: >=3.11
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: pycryptodome>=3.19
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
22
|
+
Requires-Dist: pytest-vcr>=1.0; extra == "dev"
|
|
23
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
24
|
+
Requires-Dist: python-dotenv>=1.0; extra == "dev"
|
|
25
|
+
|
|
26
|
+
# cookiecloud_decrypt
|
|
27
|
+
|
|
28
|
+
CookieCloud 数据解密 SDK,纯 Python,无其他运行时依赖。
|
|
29
|
+
|
|
30
|
+
支持两种加密模式:
|
|
31
|
+
|
|
32
|
+
- **aes-128-cbc-fixed**:新版 CookieCloud 服务器(MD5(key) + 零 IV)
|
|
33
|
+
- **legacy**:旧版使用(MD5 chain + Salted__ header,与 pycookiecloud 兼容)
|
|
34
|
+
|
|
35
|
+
## 安装
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install cookiecloud_decrypt
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 快速开始
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from cookiecloud_decrypt import decrypt, to_playwright_cookies, to_cookie_str
|
|
45
|
+
|
|
46
|
+
# 1. 从 CookieCloud 服务端获取加密数据(任意 HTTP 库)
|
|
47
|
+
import httpx
|
|
48
|
+
resp = httpx.get(
|
|
49
|
+
"https://your-server.com/get/YOUR_UUID",
|
|
50
|
+
params={"password": "YOUR_PASSWORD"},
|
|
51
|
+
)
|
|
52
|
+
encrypted = resp.json()["encrypted"]
|
|
53
|
+
|
|
54
|
+
# 2. 解密(auto 模式自动识别格式)
|
|
55
|
+
data = decrypt(encrypted, uuid="YOUR_UUID", password="YOUR_PASSWORD")
|
|
56
|
+
|
|
57
|
+
# 3. 格式化输出
|
|
58
|
+
# Playwright add_cookies 格式
|
|
59
|
+
cookies = to_playwright_cookies(data, domains=[".example.com"])
|
|
60
|
+
|
|
61
|
+
# "k1=v1; k2=v2" 字符串格式
|
|
62
|
+
cookie_str = to_cookie_str(data, domain=".example.com", keys=["a1", "session"])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
### `decrypt(encrypted, uuid, password, mode="auto")`
|
|
68
|
+
|
|
69
|
+
解密 CookieCloud 服务端返回的加密数据。
|
|
70
|
+
|
|
71
|
+
| 参数 | 类型 | 说明 |
|
|
72
|
+
|------|------|------|
|
|
73
|
+
| `encrypted` | `str` | CookieCloud GET `/get/{uuid}` 返回的 `encrypted` 字段(base64) |
|
|
74
|
+
| `uuid` | `str` | CookieCloud UUID |
|
|
75
|
+
| `password` | `str` | CookieCloud 密码 |
|
|
76
|
+
| `mode` | `str` | 解密模式:`"auto"`(默认,自动检测)、`"legacy"`、`"aes-128-cbc-fixed"` |
|
|
77
|
+
|
|
78
|
+
**返回值**:解密后的 dict,结构为 `{"cookie_data": {域名: [CookieEntry, ...]}, "update_time": ...}`
|
|
79
|
+
|
|
80
|
+
**异常**:
|
|
81
|
+
- `InvalidKeyError` — UUID 或密码错误
|
|
82
|
+
- `CorruptedDataError` — 密文格式损坏
|
|
83
|
+
|
|
84
|
+
### `to_playwright_cookies(data, domains=None)`
|
|
85
|
+
|
|
86
|
+
导出为 Playwright `BrowserContext.add_cookies()` 所需格式。
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from playwright.async_api import async_playwright
|
|
90
|
+
|
|
91
|
+
cookies = to_playwright_cookies(data, domains=[".xiaohongshu.com"])
|
|
92
|
+
async with async_playwright() as p:
|
|
93
|
+
ctx = await p.chromium.launch()
|
|
94
|
+
await ctx.add_cookies(cookies)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `to_cookie_str(data, domain, keys=None)`
|
|
98
|
+
|
|
99
|
+
导出为 `k1=v1; k2=v2` 格式的字符串。
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
cookie_str = to_cookie_str(
|
|
103
|
+
data,
|
|
104
|
+
domain=".xiaohongshu.com",
|
|
105
|
+
keys=["a1", "web_session"],
|
|
106
|
+
)
|
|
107
|
+
# → "a1=xxx; web_session=yyy"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `to_cookie_dict(data, domains=None)`
|
|
111
|
+
|
|
112
|
+
导出为 `{域名: [CookieEntry, ...]}` 格式。
|
|
113
|
+
|
|
114
|
+
## 开发
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# 安装
|
|
118
|
+
pip install -e ".[dev]"
|
|
119
|
+
|
|
120
|
+
# 运行测试
|
|
121
|
+
pytest tests/ -v
|
|
122
|
+
|
|
123
|
+
# 端到端测试(首次需要联网录制 cassette)
|
|
124
|
+
cp .env.example .env
|
|
125
|
+
# 填入真实 COOKIECLOUD_SERVER / COOKIECLOUD_UUID / COOKIECLOUD_PASSWORD
|
|
126
|
+
pytest tests/e2e_test.py -v --vcr-record=new_episodes
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 发布到 PyPI
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# 1. 安装构建工具
|
|
133
|
+
pip install build twine
|
|
134
|
+
|
|
135
|
+
# 2. 构建 wheel 和 sdist
|
|
136
|
+
python -m build
|
|
137
|
+
|
|
138
|
+
# 3. 上传到 TestPyPI(先测)
|
|
139
|
+
twine upload --repository testpypi dist/*
|
|
140
|
+
|
|
141
|
+
# 4. 上传到正式 PyPI
|
|
142
|
+
twine upload dist/*
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
发布后外部用户直接 `pip install cookiecloud_decrypt` 即可使用。
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# cookiecloud_decrypt
|
|
2
|
+
|
|
3
|
+
CookieCloud 数据解密 SDK,纯 Python,无其他运行时依赖。
|
|
4
|
+
|
|
5
|
+
支持两种加密模式:
|
|
6
|
+
|
|
7
|
+
- **aes-128-cbc-fixed**:新版 CookieCloud 服务器(MD5(key) + 零 IV)
|
|
8
|
+
- **legacy**:旧版使用(MD5 chain + Salted__ header,与 pycookiecloud 兼容)
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install cookiecloud_decrypt
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 快速开始
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from cookiecloud_decrypt import decrypt, to_playwright_cookies, to_cookie_str
|
|
20
|
+
|
|
21
|
+
# 1. 从 CookieCloud 服务端获取加密数据(任意 HTTP 库)
|
|
22
|
+
import httpx
|
|
23
|
+
resp = httpx.get(
|
|
24
|
+
"https://your-server.com/get/YOUR_UUID",
|
|
25
|
+
params={"password": "YOUR_PASSWORD"},
|
|
26
|
+
)
|
|
27
|
+
encrypted = resp.json()["encrypted"]
|
|
28
|
+
|
|
29
|
+
# 2. 解密(auto 模式自动识别格式)
|
|
30
|
+
data = decrypt(encrypted, uuid="YOUR_UUID", password="YOUR_PASSWORD")
|
|
31
|
+
|
|
32
|
+
# 3. 格式化输出
|
|
33
|
+
# Playwright add_cookies 格式
|
|
34
|
+
cookies = to_playwright_cookies(data, domains=[".example.com"])
|
|
35
|
+
|
|
36
|
+
# "k1=v1; k2=v2" 字符串格式
|
|
37
|
+
cookie_str = to_cookie_str(data, domain=".example.com", keys=["a1", "session"])
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## API
|
|
41
|
+
|
|
42
|
+
### `decrypt(encrypted, uuid, password, mode="auto")`
|
|
43
|
+
|
|
44
|
+
解密 CookieCloud 服务端返回的加密数据。
|
|
45
|
+
|
|
46
|
+
| 参数 | 类型 | 说明 |
|
|
47
|
+
|------|------|------|
|
|
48
|
+
| `encrypted` | `str` | CookieCloud GET `/get/{uuid}` 返回的 `encrypted` 字段(base64) |
|
|
49
|
+
| `uuid` | `str` | CookieCloud UUID |
|
|
50
|
+
| `password` | `str` | CookieCloud 密码 |
|
|
51
|
+
| `mode` | `str` | 解密模式:`"auto"`(默认,自动检测)、`"legacy"`、`"aes-128-cbc-fixed"` |
|
|
52
|
+
|
|
53
|
+
**返回值**:解密后的 dict,结构为 `{"cookie_data": {域名: [CookieEntry, ...]}, "update_time": ...}`
|
|
54
|
+
|
|
55
|
+
**异常**:
|
|
56
|
+
- `InvalidKeyError` — UUID 或密码错误
|
|
57
|
+
- `CorruptedDataError` — 密文格式损坏
|
|
58
|
+
|
|
59
|
+
### `to_playwright_cookies(data, domains=None)`
|
|
60
|
+
|
|
61
|
+
导出为 Playwright `BrowserContext.add_cookies()` 所需格式。
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from playwright.async_api import async_playwright
|
|
65
|
+
|
|
66
|
+
cookies = to_playwright_cookies(data, domains=[".xiaohongshu.com"])
|
|
67
|
+
async with async_playwright() as p:
|
|
68
|
+
ctx = await p.chromium.launch()
|
|
69
|
+
await ctx.add_cookies(cookies)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `to_cookie_str(data, domain, keys=None)`
|
|
73
|
+
|
|
74
|
+
导出为 `k1=v1; k2=v2` 格式的字符串。
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
cookie_str = to_cookie_str(
|
|
78
|
+
data,
|
|
79
|
+
domain=".xiaohongshu.com",
|
|
80
|
+
keys=["a1", "web_session"],
|
|
81
|
+
)
|
|
82
|
+
# → "a1=xxx; web_session=yyy"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### `to_cookie_dict(data, domains=None)`
|
|
86
|
+
|
|
87
|
+
导出为 `{域名: [CookieEntry, ...]}` 格式。
|
|
88
|
+
|
|
89
|
+
## 开发
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# 安装
|
|
93
|
+
pip install -e ".[dev]"
|
|
94
|
+
|
|
95
|
+
# 运行测试
|
|
96
|
+
pytest tests/ -v
|
|
97
|
+
|
|
98
|
+
# 端到端测试(首次需要联网录制 cassette)
|
|
99
|
+
cp .env.example .env
|
|
100
|
+
# 填入真实 COOKIECLOUD_SERVER / COOKIECLOUD_UUID / COOKIECLOUD_PASSWORD
|
|
101
|
+
pytest tests/e2e_test.py -v --vcr-record=new_episodes
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 发布到 PyPI
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# 1. 安装构建工具
|
|
108
|
+
pip install build twine
|
|
109
|
+
|
|
110
|
+
# 2. 构建 wheel 和 sdist
|
|
111
|
+
python -m build
|
|
112
|
+
|
|
113
|
+
# 3. 上传到 TestPyPI(先测)
|
|
114
|
+
twine upload --repository testpypi dist/*
|
|
115
|
+
|
|
116
|
+
# 4. 上传到正式 PyPI
|
|
117
|
+
twine upload dist/*
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
发布后外部用户直接 `pip install cookiecloud_decrypt` 即可使用。
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cookiecloud_decrypt
|
|
3
|
+
~~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
CookieCloud 数据解密 SDK。
|
|
6
|
+
|
|
7
|
+
支持两种加密模式:
|
|
8
|
+
|
|
9
|
+
- **aes-128-cbc-fixed**: 新版 CookieCloud 服务器(MD5(key) + 零 IV)
|
|
10
|
+
- **legacy**: 旧版(MD5 chain + Salted__ header,兼容 pycookiecloud)
|
|
11
|
+
|
|
12
|
+
用法::
|
|
13
|
+
|
|
14
|
+
from cookiecloud_decrypt import decrypt, to_playwright_cookies, to_cookie_str
|
|
15
|
+
|
|
16
|
+
# 解密
|
|
17
|
+
data = decrypt(encrypted="BASE64STRING...", uuid="...", password="...")
|
|
18
|
+
|
|
19
|
+
# Playwright 格式
|
|
20
|
+
cookies = to_playwright_cookies(data, domains=[".example.com"])
|
|
21
|
+
|
|
22
|
+
# "k1=v1; k2=v2" 格式
|
|
23
|
+
cookie_str = to_cookie_str(data, domain="example.com", keys=["a1", "b2"])
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from .decrypt import (
|
|
27
|
+
decrypt,
|
|
28
|
+
encrypt_legacy,
|
|
29
|
+
encrypt_aes_128_cbc_fixed,
|
|
30
|
+
DecryptMode,
|
|
31
|
+
CookieCloudDecryptError,
|
|
32
|
+
InvalidKeyError,
|
|
33
|
+
CorruptedDataError,
|
|
34
|
+
)
|
|
35
|
+
from .format import (
|
|
36
|
+
to_cookie_dict,
|
|
37
|
+
to_playwright_cookies,
|
|
38
|
+
to_cookie_str,
|
|
39
|
+
)
|
|
40
|
+
from .models import (
|
|
41
|
+
CookieEntry,
|
|
42
|
+
CookieCloudData,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
# 解密
|
|
47
|
+
"decrypt",
|
|
48
|
+
"encrypt_legacy",
|
|
49
|
+
"encrypt_aes_128_cbc_fixed",
|
|
50
|
+
"DecryptMode",
|
|
51
|
+
"CookieCloudDecryptError",
|
|
52
|
+
"InvalidKeyError",
|
|
53
|
+
"CorruptedDataError",
|
|
54
|
+
# 格式化
|
|
55
|
+
"to_cookie_dict",
|
|
56
|
+
"to_playwright_cookies",
|
|
57
|
+
"to_cookie_str",
|
|
58
|
+
# 类型
|
|
59
|
+
"CookieEntry",
|
|
60
|
+
"CookieCloudData",
|
|
61
|
+
]
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CookieCloud 数据解密核心实现。
|
|
3
|
+
|
|
4
|
+
支持两种加密模式:
|
|
5
|
+
- aes-128-cbc-fixed: 新版 CookieCloud 服务器(MD5(key) + 零 IV)
|
|
6
|
+
- legacy: 旧版使用(MD5 chain + Salted__ header,兼容 pycookiecloud)
|
|
7
|
+
|
|
8
|
+
加密流程由 CookieCloud 服务端完成,参考:
|
|
9
|
+
https://github.com/easychen/CookieCloud
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import base64
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from Cryptodome.Cipher import AES
|
|
21
|
+
from Cryptodome.Util.Padding import unpad
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DecryptMode(str, Enum):
|
|
25
|
+
"""解密模式。"""
|
|
26
|
+
LEGACY = "legacy"
|
|
27
|
+
"""旧版模式:MD5 chain + Salted__ header,与 pycookiecloud 兼容。"""
|
|
28
|
+
AES_128_CBC_FIXED = "aes-128-cbc-fixed"
|
|
29
|
+
"""新版模式:MD5(key) + 零 IV。"""
|
|
30
|
+
AUTO = "auto"
|
|
31
|
+
"""自动检测:优先尝试 aes-128-cbc-fixed,再尝试 legacy。"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CookieCloudDecryptError(Exception):
|
|
35
|
+
"""解密失败基类。"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class InvalidKeyError(CookieCloudDecryptError):
|
|
40
|
+
"""密钥错误(密码/UUID 不正确)。"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CorruptedDataError(CookieCloudDecryptError):
|
|
45
|
+
"""数据损坏(密文格式异常)。"""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# 内部工具
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
SALTED_HEADER = b"Salted__"
|
|
54
|
+
"""OpenSSL salted format header (legacy 模式)。"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _derive_key_md5(data: bytes, salt: bytes, output: int = 48) -> bytes:
|
|
58
|
+
"""
|
|
59
|
+
MD5 chain key derivation — legacy 模式使用。
|
|
60
|
+
等价于 OpenSSL EVP_BytesToKey(MD5)。
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
data: 初始种子(通常是 key + salt)
|
|
64
|
+
salt: 8 字节盐
|
|
65
|
+
output: 需要输出的总字节数
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
key || iv (前 32 字节为 key,后 16 字节为 IV)
|
|
69
|
+
"""
|
|
70
|
+
result = b""
|
|
71
|
+
prev = b""
|
|
72
|
+
while len(result) < output:
|
|
73
|
+
prev = hashlib.md5(prev + data + salt).digest()
|
|
74
|
+
result += prev
|
|
75
|
+
return result[:output]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _derive_key_fixed(key: str) -> bytes:
|
|
79
|
+
"""
|
|
80
|
+
MD5(key) key derivation — aes-128-cbc-fixed 模式使用。
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
key: UUID + '-' + password 的 MD5 十六进制字符串的前 16 字符
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
16 字节密钥
|
|
87
|
+
"""
|
|
88
|
+
return hashlib.md5(key.encode()).digest()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# ---------------------------------------------------------------------------
|
|
92
|
+
# 加密函数(测试用,生产环境不需要)
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
def encrypt_legacy(plaintext: str, uuid: str, password: str) -> str:
|
|
96
|
+
"""
|
|
97
|
+
用 legacy 模式加密数据(用于生成测试 fixture)。
|
|
98
|
+
流程与 CookieCloud 服务端一致。
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
plaintext: JSON 字符串
|
|
102
|
+
uuid: CookieCloud UUID
|
|
103
|
+
password: CookieCloud 密码
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
base64 编码后的密文
|
|
107
|
+
"""
|
|
108
|
+
import os
|
|
109
|
+
|
|
110
|
+
key_str = f"{uuid}-{password}"
|
|
111
|
+
hash_hex = hashlib.md5(key_str.encode()).hexdigest()
|
|
112
|
+
key = hash_hex[:16].encode()
|
|
113
|
+
|
|
114
|
+
salt = os.urandom(8)
|
|
115
|
+
key_iv = _derive_key_md5(key, salt, output=48)
|
|
116
|
+
aes_key, aes_iv = key_iv[:32], key_iv[32:]
|
|
117
|
+
|
|
118
|
+
padded = _pad(plaintext.encode("utf-8"))
|
|
119
|
+
cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
|
|
120
|
+
ciphertext = cipher.encrypt(padded)
|
|
121
|
+
|
|
122
|
+
return base64.b64encode(b"Salted__" + salt + ciphertext).decode()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def encrypt_aes_128_cbc_fixed(plaintext: str, uuid: str, password: str) -> str:
|
|
126
|
+
"""
|
|
127
|
+
用 aes-128-cbc-fixed 模式加密数据(用于生成测试 fixture)。
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
plaintext: JSON 字符串
|
|
131
|
+
uuid: CookieCloud UUID
|
|
132
|
+
password: CookieCloud 密码
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
base64 编码后的密文
|
|
136
|
+
"""
|
|
137
|
+
import os
|
|
138
|
+
|
|
139
|
+
key_str = f"{uuid}-{password}"
|
|
140
|
+
hash_hex = hashlib.md5(key_str.encode()).hexdigest()
|
|
141
|
+
key = hash_hex[:16].encode()
|
|
142
|
+
|
|
143
|
+
aes_key = _derive_key_fixed(key_str)
|
|
144
|
+
zero_iv = b"\x00" * 16
|
|
145
|
+
|
|
146
|
+
padded = _pad(plaintext.encode("utf-8"))
|
|
147
|
+
cipher = AES.new(aes_key, AES.MODE_CBC, zero_iv)
|
|
148
|
+
ciphertext = cipher.encrypt(padded)
|
|
149
|
+
|
|
150
|
+
return base64.b64encode(ciphertext).decode()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _pad(data: bytes) -> bytes:
|
|
154
|
+
"""PKCS#7 padding。"""
|
|
155
|
+
block_size = 16
|
|
156
|
+
pad_len = block_size - (len(data) % block_size)
|
|
157
|
+
return data + bytes([pad_len] * pad_len)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# 解密函数
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
def _decrypt_legacy(encrypted_b64: str, uuid: str, password: str) -> dict[str, Any]:
|
|
165
|
+
"""
|
|
166
|
+
用 legacy 模式解密。
|
|
167
|
+
与 pycookiecloud PyCryptoJS.py 的 decrypt() 完全兼容。
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
CorruptedDataError: 密文格式损坏(Salted__ header 不匹配)
|
|
171
|
+
InvalidKeyError: 密钥错误(解密后不是合法 JSON)
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
encrypted = base64.b64decode(encrypted_b64)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
raise CorruptedDataError(f"base64 decode failed: {e}") from e
|
|
177
|
+
|
|
178
|
+
if not encrypted.startswith(SALTED_HEADER):
|
|
179
|
+
raise CorruptedDataError(
|
|
180
|
+
f"Missing OpenSSL salted header. Expected {SALTED_HEADER!r}, "
|
|
181
|
+
f"got {encrypted[:8]!r}. Data may use aes-128-cbc-fixed mode instead."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
salt = encrypted[8:16]
|
|
185
|
+
ciphertext = encrypted[16:]
|
|
186
|
+
|
|
187
|
+
key_str = f"{uuid}-{password}"
|
|
188
|
+
hash_hex = hashlib.md5(key_str.encode()).hexdigest()
|
|
189
|
+
key = hash_hex[:16].encode()
|
|
190
|
+
|
|
191
|
+
key_iv = _derive_key_md5(key, salt, output=48)
|
|
192
|
+
aes_key, aes_iv = key_iv[:32], key_iv[32:]
|
|
193
|
+
|
|
194
|
+
try:
|
|
195
|
+
cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
|
|
196
|
+
padded = cipher.decrypt(ciphertext)
|
|
197
|
+
plaintext = unpad(padded, AES.block_size)
|
|
198
|
+
return json.loads(plaintext.decode("utf-8"))
|
|
199
|
+
except (ValueError, json.JSONDecodeError) as e:
|
|
200
|
+
raise InvalidKeyError(
|
|
201
|
+
f"Decryption failed — wrong key or corrupted data. "
|
|
202
|
+
f"Verify uuid and password are correct. Cause: {e}"
|
|
203
|
+
) from e
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _decrypt_aes_128_cbc_fixed(encrypted_b64: str, uuid: str, password: str) -> dict[str, Any]:
|
|
207
|
+
"""
|
|
208
|
+
用 aes-128-cbc-fixed 模式解密。
|
|
209
|
+
新版 CookieCloud 服务器使用此模式。
|
|
210
|
+
|
|
211
|
+
Raises:
|
|
212
|
+
CorruptedDataError: 密文格式损坏(长度不是 16 的倍数)
|
|
213
|
+
InvalidKeyError: 密钥错误(解密后不是合法 JSON)
|
|
214
|
+
"""
|
|
215
|
+
try:
|
|
216
|
+
encrypted = base64.b64decode(encrypted_b64)
|
|
217
|
+
except Exception as e:
|
|
218
|
+
raise CorruptedDataError(f"base64 decode failed: {e}") from e
|
|
219
|
+
|
|
220
|
+
if len(encrypted) % 16 != 0:
|
|
221
|
+
raise CorruptedDataError(
|
|
222
|
+
f"Encrypted data length ({len(encrypted)}) is not a multiple of 16. "
|
|
223
|
+
f"Data may use legacy mode instead."
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
key_str = f"{uuid}-{password}"
|
|
227
|
+
aes_key = _derive_key_fixed(key_str)
|
|
228
|
+
zero_iv = b"\x00" * 16
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
cipher = AES.new(aes_key, AES.MODE_CBC, zero_iv)
|
|
232
|
+
padded = cipher.decrypt(encrypted)
|
|
233
|
+
plaintext = unpad(padded, AES.block_size)
|
|
234
|
+
return json.loads(plaintext.decode("utf-8"))
|
|
235
|
+
except (ValueError, json.JSONDecodeError) as e:
|
|
236
|
+
# ValueError 可能是:1) wrong key(解密内容非合法 PKCS#7),
|
|
237
|
+
# 2) wrong format(legacy 数据用 fixed 模式解密)。
|
|
238
|
+
# 用 Salted__ header 区分:legacy 格式有 header,fixed 没有。
|
|
239
|
+
# 若有 header 说明是 legacy 格式未匹配,转 CorruptedDataError 以便 auto fallback;
|
|
240
|
+
# 否则视为密钥错误。
|
|
241
|
+
if encrypted.startswith(SALTED_HEADER):
|
|
242
|
+
raise CorruptedDataError(
|
|
243
|
+
f"Data has OpenSSL salted header — appears to be legacy format. "
|
|
244
|
+
f"Retry with mode='legacy'."
|
|
245
|
+
) from e
|
|
246
|
+
raise InvalidKeyError(
|
|
247
|
+
f"Decryption failed — wrong key or corrupted data. "
|
|
248
|
+
f"Verify uuid and password are correct. Cause: {e}"
|
|
249
|
+
) from e
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def decrypt(
|
|
253
|
+
encrypted: str,
|
|
254
|
+
uuid: str,
|
|
255
|
+
password: str,
|
|
256
|
+
mode: DecryptMode | str = DecryptMode.AUTO,
|
|
257
|
+
) -> dict[str, Any]:
|
|
258
|
+
"""
|
|
259
|
+
解密 CookieCloud 服务端返回的加密数据。
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
encrypted: CookieCloud GET /get/{uuid} 返回的 ``encrypted`` 字段(base64)
|
|
263
|
+
uuid: CookieCloud UUID
|
|
264
|
+
password: CookieCloud 密码
|
|
265
|
+
mode: 解密模式,默认 auto(自动检测)
|
|
266
|
+
|
|
267
|
+
- ``"auto"``: 优先尝试 aes-128-cbc-fixed,再尝试 legacy
|
|
268
|
+
- ``"legacy"``: 仅使用 legacy 模式(兼容 pycookiecloud)
|
|
269
|
+
- ``"aes-128-cbc-fixed"``: 仅使用新版模式
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
解密后的原始 JSON dict(含 ``cookie_data`` 等顶层字段)
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
CookieCloudDecryptError: 解密失败
|
|
276
|
+
InvalidKeyError: 密钥错误(uuid 或 password 不正确)
|
|
277
|
+
CorruptedDataError: 密文格式损坏
|
|
278
|
+
|
|
279
|
+
Example::
|
|
280
|
+
|
|
281
|
+
from cookiecloud_decrypt import decrypt
|
|
282
|
+
|
|
283
|
+
data = decrypt(
|
|
284
|
+
encrypted="BASE64STRING...",
|
|
285
|
+
uuid="your-uuid",
|
|
286
|
+
password="your-password",
|
|
287
|
+
)
|
|
288
|
+
cookie_dict = data["cookie_data"]
|
|
289
|
+
"""
|
|
290
|
+
if isinstance(mode, str):
|
|
291
|
+
mode = DecryptMode(mode.lower())
|
|
292
|
+
|
|
293
|
+
if mode == DecryptMode.AUTO:
|
|
294
|
+
# 优先尝试 aes-128-cbc-fixed(新版)
|
|
295
|
+
try:
|
|
296
|
+
return _decrypt_aes_128_cbc_fixed(encrypted, uuid, password)
|
|
297
|
+
except CorruptedDataError:
|
|
298
|
+
# 不是 fixed 格式,尝试 legacy
|
|
299
|
+
return _decrypt_legacy(encrypted, uuid, password)
|
|
300
|
+
# InvalidKeyError 直接抛出,不 fallback(密钥错误时 legacy 也会失败)
|
|
301
|
+
|
|
302
|
+
elif mode == DecryptMode.AES_128_CBC_FIXED:
|
|
303
|
+
return _decrypt_aes_128_cbc_fixed(encrypted, uuid, password)
|
|
304
|
+
|
|
305
|
+
elif mode == DecryptMode.LEGACY:
|
|
306
|
+
return _decrypt_legacy(encrypted, uuid, password)
|
|
307
|
+
|
|
308
|
+
else:
|
|
309
|
+
raise ValueError(f"Unknown decrypt mode: {mode}")
|