cookiecloud-decrypt 0.1.0__py3-none-any.whl

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.
@@ -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}")
@@ -0,0 +1,222 @@
1
+ """
2
+ CookieCloud 解密结果的格式化工具。
3
+ 支持 dict / Playwright add_cookies / "k1=v1; k2=v2" 三种导出格式。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ from .models import CookieCloudData, CookieEntry
11
+
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # 内部工具
15
+ # ---------------------------------------------------------------------------
16
+
17
+ _COOKIE_PRIORITY_DEFAULT = "Medium"
18
+ """CookieCloud 默认 priority。"""
19
+
20
+ _SAME_SITE_DEFAULT = "Lax"
21
+ """Playwright add_cookies 默认 sameSite。"""
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # 导出函数
26
+ # ---------------------------------------------------------------------------
27
+
28
+ def to_cookie_dict(
29
+ data: CookieCloudData,
30
+ domains: list[str] | None = None,
31
+ *,
32
+ unwrap: bool = True,
33
+ ) -> dict[str, list[CookieEntry]]:
34
+ """
35
+ 将解密结果导出为 ``{域名: [CookieEntry, ...]}`` 格式。
36
+
37
+ Args:
38
+ data: :func:`~cookiecloud_decrypt.decrypt` 的返回值
39
+ domains: 仅导出的域名白名单,默认为全部域名。
40
+ 支持精确匹配(如 ``"xiaohongshu.com"``)和通配符
41
+ (如 ``".xiaohongshu.com"``,以点开头表示匹配该域及子域)。
42
+ unwrap: 若为 ``True``(默认),自动去掉 ``data["cookie_data"]`` 外层;
43
+ 若为 ``False``,直接使用原始返回值(不含 ``cookie_data`` 包装)。
44
+
45
+ Returns:
46
+ ``{域名: [CookieEntry, ...]}``
47
+
48
+ Example::
49
+
50
+ cookie_dict = to_cookie_dict(
51
+ data,
52
+ domains=[".xiaohongshu.com", "xiaohongshu.com"],
53
+ )
54
+ """
55
+ cookie_data = data if not unwrap else data.get("cookie_data", data)
56
+
57
+ if domains is None:
58
+ return dict(cookie_data)
59
+
60
+ result: dict[str, list[CookieEntry]] = {}
61
+ for domain in cookie_data:
62
+ # 域名匹配
63
+ if not _domain_matches(domain, domains):
64
+ continue
65
+ result[domain] = cookie_data[domain]
66
+
67
+ return result
68
+
69
+
70
+ def to_playwright_cookies(
71
+ data: CookieCloudData,
72
+ domains: list[str] | None = None,
73
+ *,
74
+ unwrap: bool = True,
75
+ ) -> list[dict[str, Any]]:
76
+ """
77
+ 将解密结果导出为 Playwright ``BrowserContext.add_cookies()`` 所需格式。
78
+
79
+ Args:
80
+ data: :func:`~cookiecloud_decrypt.decrypt` 的返回值
81
+ domains: 仅导出的域名白名单,默认为全部域名
82
+ unwrap: 若为 ``True``(默认),自动去掉 ``data["cookie_data"]`` 外层
83
+
84
+ Returns:
85
+ Playwright add_cookies 格式的 cookie 列表::
86
+
87
+ [
88
+ {
89
+ "name": "a1",
90
+ "value": "xxx",
91
+ "domain": "xiaohongshu.com",
92
+ "path": "/",
93
+ "secure": True,
94
+ "httpOnly": False,
95
+ "sameSite": "Lax",
96
+ },
97
+ ...
98
+ ]
99
+
100
+ Note:
101
+ Playwright add_cookies 要求 domain 不带前导点。
102
+ 此函数会自动去掉 ``.xiaohongshu.com`` → ``xiaohongshu.com``。
103
+
104
+ Example::
105
+
106
+ from playwright.async_api import async_playwright
107
+
108
+ cookies = to_playwright_cookies(data, domains=[".xiaohongshu.com"])
109
+ async with async_playwright() as p:
110
+ ctx = await p.chromium.launch()
111
+ await ctx.add_cookies(cookies)
112
+ """
113
+ cookie_dict = to_cookie_dict(data, domains, unwrap=unwrap)
114
+
115
+ result: list[dict[str, Any]] = []
116
+ for domain, entries in cookie_dict.items():
117
+ # Playwright 要求 domain 不带前导点
118
+ playwright_domain = domain.lstrip(".")
119
+
120
+ for entry in entries:
121
+ name = entry.get("name", "")
122
+ value = entry.get("value", "")
123
+ if not name:
124
+ continue
125
+
126
+ result.append({
127
+ "name": name,
128
+ "value": value,
129
+ "domain": playwright_domain,
130
+ "path": entry.get("path", "/"),
131
+ "secure": entry.get("secure", True),
132
+ "httpOnly": entry.get("httpOnly", False),
133
+ "sameSite": entry.get("sameSite") or _SAME_SITE_DEFAULT,
134
+ })
135
+
136
+ return result
137
+
138
+
139
+ def to_cookie_str(
140
+ data: CookieCloudData,
141
+ domain: str,
142
+ keys: list[str] | None = None,
143
+ *,
144
+ unwrap: bool = True,
145
+ separator: str = "; ",
146
+ ends_with_separator: bool = False,
147
+ ) -> str:
148
+ """
149
+ 将解密结果导出为 ``"k1=v1; k2=v2"`` 格式的 cookie 字符串。
150
+
151
+ Args:
152
+ data: :func:`~cookiecloud_decrypt.decrypt` 的返回值
153
+ domain: 目标域名(如 ``"xiaohongshu.com"`` 或 ``".xiaohongshu.com"``)。
154
+ 会自动尝试带点和不带点两种形式匹配。
155
+ keys: 仅导出的 cookie 名称白名单,默认为全部。
156
+ unwrap: 若为 ``True``(默认),自动去掉 ``data["cookie_data"]`` 外层
157
+ separator: key=value 对之间的分隔符,默认为 ``"; "``
158
+ ends_with_separator: 若为 ``True``,结果以分隔符结尾
159
+
160
+ Returns:
161
+ cookie 字符串,如 ``"a1=xxx; web_session=yyy"``
162
+
163
+ Example::
164
+
165
+ cookie_str = to_cookie_str(
166
+ data,
167
+ domain=".xiaohongshu.com",
168
+ keys=["a1", "web_session"],
169
+ )
170
+ """
171
+ cookie_data = data if not unwrap else data.get("cookie_data", data)
172
+
173
+ # 域名匹配
174
+ entries: list[CookieEntry] = []
175
+ for d, entry_list in cookie_data.items():
176
+ if _domain_matches(d, [domain]):
177
+ entries.extend(entry_list)
178
+ break # 匹配到第一个即停止
179
+
180
+ if not entries:
181
+ return ""
182
+
183
+ parts: list[str] = []
184
+ for entry in entries:
185
+ name = entry.get("name", "")
186
+ value = entry.get("value", "")
187
+ if not name:
188
+ continue
189
+ if keys is not None and name not in keys:
190
+ continue
191
+ parts.append(f"{name}={value}")
192
+
193
+ result = separator.join(parts)
194
+ if ends_with_separator and result and not result.endswith(separator):
195
+ result += separator
196
+
197
+ return result
198
+
199
+
200
+ # ---------------------------------------------------------------------------
201
+ # 内部工具
202
+ # ---------------------------------------------------------------------------
203
+
204
+ def _domain_matches(domain: str, patterns: list[str]) -> bool:
205
+ """
206
+ 判断 domain 是否匹配 patterns 中的任意一个。
207
+
208
+ 匹配规则:
209
+ - 精确匹配:``"xiaohongshu.com"`` 匹配 ``"xiaohongshu.com"``
210
+ - 带点前缀:``".xiaohongshu.com"`` 匹配 ``"xiaohongshu.com"`` 及所有子域
211
+ - 子域匹配:``"www.xiaohongshu.com"`` 匹配 ``".xiaohongshu.com"``
212
+ """
213
+ domain_clean = domain.lstrip(".")
214
+
215
+ for pattern in patterns:
216
+ pattern_clean = pattern.lstrip(".")
217
+ if pattern_clean == domain_clean:
218
+ return True
219
+ if pattern.startswith(".") and domain.endswith(pattern_clean):
220
+ return True
221
+
222
+ return False
@@ -0,0 +1,40 @@
1
+ """
2
+ CookieCloud 数据类型定义。
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, TypedDict
8
+
9
+
10
+ class CookieEntry(TypedDict, total=False):
11
+ """CookieCloud 返回的单条 Cookie 条目。"""
12
+ name: str
13
+ value: str
14
+ domain: str
15
+ path: str
16
+ expires: int
17
+ size: int
18
+ httpOnly: bool
19
+ secure: bool
20
+ sameSite: str
21
+ session: bool
22
+ priority: str
23
+ sameParty: bool
24
+ sourceScheme: str
25
+ sourcePort: int
26
+
27
+
28
+ # CookieCloud 原始解密结果结构(最外层 JSON)
29
+ CookieCloudData = dict[str, Any]
30
+ """
31
+ 解密后的完整 JSON dict,结构为::
32
+
33
+ {
34
+ "cookie_data": {
35
+ ".example.com": [CookieEntry, ...],
36
+ "www.example.com": [CookieEntry, ...],
37
+ },
38
+ "update_time": 1234567890
39
+ }
40
+ """
@@ -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,8 @@
1
+ cookiecloud_decrypt/__init__.py,sha256=DsNr-sNp0WHuEwk7ybZGFh58do_Euae7rvZ9WwnVp18,1330
2
+ cookiecloud_decrypt/decrypt.py,sha256=IuBsmL6V2vBtDM_W0BNt6qBh0gmFiG7VspTMHYoLIbM,9799
3
+ cookiecloud_decrypt/format.py,sha256=zfFDog6r4AKEzkgPF6PTB6Bz5duEBKVUtTqUo77xLGs,6948
4
+ cookiecloud_decrypt/models.py,sha256=-VIvwDN0RLZ9pG0hscO7nl-jeVwV3SeJ6t9Fbu3x1N0,782
5
+ cookiecloud_decrypt-0.1.0.dist-info/METADATA,sha256=lrSdy1zZFEg-QJmuLB8-9SjLvIsUVSRxn84aYQMokBQ,4080
6
+ cookiecloud_decrypt-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ cookiecloud_decrypt-0.1.0.dist-info/top_level.txt,sha256=vWhyzS-8JH12XXUEkTHtK-AYtpFkSdcpylycM_plLgs,20
8
+ cookiecloud_decrypt-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ cookiecloud_decrypt