├── .bak.travis.yml ├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── PyCookieCloud ├── PyCookieCloud.py ├── PyCryptoJS.py └── __init__.py ├── README.md ├── setup.cfg └── setup.py /.bak.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - '3.9' 4 | script: 5 | - echo "Hello World" 6 | deploy: 7 | provider: pypi 8 | username: "__token__" 9 | password: "$PYPI_TOKEN" 10 | on: 11 | branch: master 12 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | push: 13 | branches: 14 | master 15 | workflow_dispatch: 16 | 17 | permissions: 18 | contents: read 19 | 20 | jobs: 21 | deploy: 22 | 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Set up Python 28 | uses: actions/setup-python@v3 29 | with: 30 | python-version: '3.x' 31 | - name: Install dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install build 35 | - name: Build package 36 | run: python -m build 37 | - name: Publish package 38 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 39 | with: 40 | user: __token__ 41 | password: ${{ secrets.PYPI_API_TOKEN }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /PyCookieCloud.iml 2 | /.idea/ 3 | /PyCookieCloud.egg-info/ 4 | /**/__pycache__/ 5 | /dist/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 lupohan44 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /PyCookieCloud/PyCookieCloud.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import hashlib 3 | import json 4 | from typing import Optional, Dict, List, Any 5 | from urllib.parse import urljoin,urlparse 6 | from pathlib import PurePosixPath 7 | 8 | import requests 9 | 10 | from .PyCryptoJS import encrypt, decrypt 11 | 12 | 13 | class PyCookieCloud: 14 | def __init__(self, url: str, uuid: str, password: str): 15 | self.url: str = url 16 | self.uuid: str = uuid 17 | self.password: str = password 18 | self.api_root: str = urlparse(url).path if urlparse(url).path else '/' 19 | 20 | def check_connection(self) -> bool: 21 | """ 22 | Test the connection to the CookieCloud server. 23 | 24 | :return: True if the connection is successful, False otherwise. 25 | """ 26 | try: 27 | resp = requests.get(self.url) 28 | if resp.status_code == 200: 29 | return True 30 | else: 31 | return False 32 | except Exception as e: 33 | return False 34 | 35 | def get_encrypted_data(self) -> Optional[str]: 36 | """ 37 | Get the encrypted data from the CookieCloud server. 38 | 39 | :return: The encrypted data if the connection is successful, None otherwise. 40 | """ 41 | if self.check_connection(): 42 | path = str(PurePosixPath(self.api_root, 'get/', self.uuid)) 43 | cookie_cloud_request = requests.get(urljoin(self.url, path)) 44 | if cookie_cloud_request.status_code == 200: 45 | cookie_cloud_response = cookie_cloud_request.json() 46 | encrypted_data = cookie_cloud_response["encrypted"] 47 | return encrypted_data 48 | else: 49 | return None 50 | else: 51 | return None 52 | 53 | def get_decrypted_data(self) -> Optional[Dict[str, Any]]: 54 | """ 55 | Get the decrypted data from the CookieCloud server. 56 | 57 | :return: decrypted data if the decryption is successful, None otherwise. 58 | """ 59 | encrypted_data = self.get_encrypted_data() 60 | if encrypted_data is not None: 61 | try: 62 | decrypted_data = decrypt(encrypted_data, self.get_the_key().encode('utf-8')).decode('utf-8') 63 | decrypted_data = json.loads(decrypted_data) 64 | if 'cookie_data' in decrypted_data: 65 | return decrypted_data['cookie_data'] 66 | except Exception as e: 67 | return None 68 | else: 69 | return None 70 | 71 | def get_cookie_value(self, hostname: str, key: str) -> Optional[str]: 72 | """ 73 | Get the cookie value from the CookieCloud server. 74 | 75 | :param hostname: the hostname of the cookie. 76 | :param key: the key of the cookie. 77 | :return: the cookie value if the decryption is successful, None otherwise. 78 | """ 79 | decrypted_data = self.get_decrypted_data() 80 | if decrypted_data is not None: 81 | if hostname in decrypted_data: 82 | for value in decrypted_data[hostname]: 83 | if value['name'] == key: 84 | if 'value' in value: 85 | return value['value'] 86 | return None 87 | 88 | def get_cookie_str(self, hostname: str, keys: Optional[List[str]] = None, all_keys_required: bool = True) -> Optional[str]: 89 | """ 90 | Get the cookie string from the CookieCloud server. 91 | 92 | :param hostname: the hostname of the cookie. 93 | :param keys: the keys of the cookie. 94 | :param all_keys_required: will return None if not all keys are matched when all_keys_required is True. 95 | :return: 96 | """ 97 | decrypted_data = self.get_decrypted_data() 98 | if decrypted_data is not None: 99 | if hostname in decrypted_data: 100 | cookie_str = "" 101 | keys_matched = [] 102 | for value in decrypted_data[hostname]: 103 | if keys is None or value['name'] in keys: 104 | keys_matched.append(value['name']) 105 | cookie_str += value['name'] + '=' + value['value'] + '; ' 106 | if all_keys_required and keys is not None: 107 | if collections.Counter(keys) == collections.Counter(keys_matched): 108 | return cookie_str 109 | else: 110 | return None 111 | else: 112 | return cookie_str 113 | return None 114 | 115 | def update_cookie(self, cookie: Dict[str, Any]) -> bool: 116 | """ 117 | Update cookie data to CookieCloud. 118 | 119 | :param cookie: cookie value to update, if this cookie does not contain 'cookie_data' key, it will be added into 'cookie_data'. 120 | :return: if update success, return True, else return False. 121 | """ 122 | if 'cookie_data' not in cookie: 123 | cookie = {'cookie_data': cookie} 124 | raw_data = json.dumps(cookie) 125 | encrypted_data = encrypt(raw_data.encode('utf-8'), self.get_the_key().encode('utf-8')).decode('utf-8') 126 | cookie_cloud_request = requests.post(urljoin(self.url, '/update'), data={'uuid': self.uuid, 'encrypted': encrypted_data}) 127 | if cookie_cloud_request.status_code == 200: 128 | if cookie_cloud_request.json()['action'] == 'done': 129 | return True 130 | return False 131 | 132 | def get_the_key(self) -> str: 133 | """ 134 | Get the key used to encrypt and decrypt data. 135 | 136 | :return: the key. 137 | """ 138 | md5 = hashlib.md5() 139 | md5.update((self.uuid + '-' + self.password).encode('utf-8')) 140 | return md5.hexdigest()[:16] 141 | -------------------------------------------------------------------------------- /PyCookieCloud/PyCryptoJS.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is a copy of https://stackoverflow.com/questions/36762098/how-to-decrypt-password-from-javascript-cryptojs-aes-encryptpassword-passphras 3 | """ 4 | from Cryptodome import Random 5 | from Cryptodome.Cipher import AES 6 | import base64 7 | from hashlib import md5 8 | 9 | BLOCK_SIZE = 16 10 | 11 | 12 | def pad(data): 13 | length = BLOCK_SIZE - (len(data) % BLOCK_SIZE) 14 | return data + (chr(length) * length).encode() 15 | 16 | 17 | def unpad(data): 18 | return data[:-(data[-1] if type(data[-1]) == int else ord(data[-1]))] 19 | 20 | 21 | def bytes_to_key(data, salt, output=48): 22 | # extended from https://gist.github.com/gsakkis/4546068 23 | assert len(salt) == 8, len(salt) 24 | data += salt 25 | key = md5(data).digest() 26 | final_key = key 27 | while len(final_key) < output: 28 | key = md5(key + data).digest() 29 | final_key += key 30 | return final_key[:output] 31 | 32 | 33 | def encrypt(message, passphrase): 34 | salt = Random.new().read(8) 35 | key_iv = bytes_to_key(passphrase, salt, 32 + 16) 36 | key = key_iv[:32] 37 | iv = key_iv[32:] 38 | aes = AES.new(key, AES.MODE_CBC, iv) 39 | return base64.b64encode(b"Salted__" + salt + aes.encrypt(pad(message))) 40 | 41 | 42 | def decrypt(encrypted, passphrase): 43 | encrypted = base64.b64decode(encrypted) 44 | assert encrypted[0:8] == b"Salted__" 45 | salt = encrypted[8:16] 46 | key_iv = bytes_to_key(passphrase, salt, 32 + 16) 47 | key = key_iv[:32] 48 | iv = key_iv[32:] 49 | aes = AES.new(key, AES.MODE_CBC, iv) 50 | return unpad(aes.decrypt(encrypted[16:])) 51 | -------------------------------------------------------------------------------- /PyCookieCloud/__init__.py: -------------------------------------------------------------------------------- 1 | from .PyCookieCloud import PyCookieCloud 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Unofficial Python Wrapper Library for CookieCloud 2 | ======= 3 | [![Build Status](https://app.travis-ci.com/lupohan44/PyCookieCloud.svg?branch=master)](https://app.travis-ci.com/lupohan44/PyCookieCloud) 4 | 5 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/lupohan44) 6 | 7 | `PyCookieCloud` is is an unofficial Python wrapper library for [CookieCloud](https://github.com/easychen/CookieCloud). The decryption of data happens on the client side. 8 | 9 | Table of Content 10 | ================ 11 | 12 | * [Installation](#installation) 13 | 14 | * [Usage](#usage) 15 | 16 | * [License](#license) 17 | 18 | 19 | Installation 20 | ============ 21 | 22 | ``` 23 | pip install PyCookieCloud 24 | ``` 25 | 26 | **Windows user might need to rename your ```Python\PythonXX\Lib\site-packages\crypto``` to ```Python\PythonXX\Lib\site-packages\Crypto```** 27 | 28 | Usage 29 | ======= 30 | ```python 31 | from PyCookieCloud import PyCookieCloud 32 | 33 | 34 | def main(): 35 | cookie_cloud = PyCookieCloud('YOUR_COOKIE_CLOUD_URL', 'YOUR_COOKIE_CLOUD_UUID', 'YOUR_COOKIE_CLOUD_PASSWORD') 36 | the_key = cookie_cloud.get_the_key() 37 | if not the_key: 38 | print('Failed to get the key') 39 | return 40 | encrypted_data = cookie_cloud.get_encrypted_data() 41 | if not encrypted_data: 42 | print('Failed to get encrypted data') 43 | return 44 | decrypted_data = cookie_cloud.get_decrypted_data() 45 | if not decrypted_data: 46 | print('Failed to get decrypted data') 47 | return 48 | print(decrypted_data) 49 | another_cookie_cloud = PyCookieCloud('YOUR_COOKIE_CLOUD_URL', 'YOUR_COOKIE_CLOUD_UUID_2', 'YOUR_COOKIE_CLOUD_PASSWORD_2') 50 | if not another_cookie_cloud.update_cookie(decrypted_data): 51 | print('Failed to update cookie') 52 | return 53 | another_decrypted_data = another_cookie_cloud.get_decrypted_data() 54 | print(another_decrypted_data) 55 | print(decrypted_data == another_decrypted_data) 56 | 57 | 58 | if __name__ == '__main__': 59 | main() 60 | 61 | ``` 62 | 63 | License 64 | ======= 65 | [MIT](LICENSE) -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description_file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from setuptools import setup 5 | import sys 6 | 7 | if sys.version_info[0] != 3: 8 | sys.exit('Python 2 is not supported') 9 | 10 | setup( 11 | name='PyCookieCloud', 12 | license='MIT', 13 | version='1.0.4', 14 | author='lupohan44', 15 | author_email='cptf_lupohan@126.com', 16 | url='https://github.com/lupohan44/PyCookieCloud', 17 | description='This is an unofficial Python wrapper library for CookieCloud (https://github.com/easychen/CookieCloud).', 18 | keywords=['cookiecloud', 'cookie', 'cloud', 'cookies', 'cookiecloud-python', 'pycookiecloud'], 19 | packages=['PyCookieCloud'], 20 | install_requires=['requests', 'pycryptodomex'], 21 | long_description="This is an unofficial Python wrapper library for CookieCloud (https://github.com/easychen/CookieCloud)." 22 | ) 23 | --------------------------------------------------------------------------------