|
5 | 5 | import http.server |
6 | 6 | import json |
7 | 7 | import os |
8 | | -import secrets |
| 8 | +import platform |
9 | 9 | import subprocess |
10 | 10 | import sys |
11 | 11 | import urllib.request |
|
14 | 14 |
|
15 | 15 | SALT = b"oroio" |
16 | 16 | ITERATIONS = 10000 |
| 17 | +IS_WINDOWS = platform.system() == 'Windows' |
17 | 18 |
|
18 | | -def _ensure_crypto(): |
19 | | - """确保加密库可用,自动安装 pycryptodome""" |
20 | | - try: |
21 | | - from cryptography.hazmat.primitives.ciphers import Cipher |
22 | | - return True |
23 | | - except ImportError: |
24 | | - pass |
25 | | - try: |
26 | | - from Crypto.Cipher import AES |
27 | | - return True |
28 | | - except ImportError: |
29 | | - pass |
30 | | - # 尝试安装 pycryptodome |
31 | | - try: |
32 | | - subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-q', 'pycryptodome'], |
33 | | - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) |
34 | | - # 安装后清除导入缓存并重试 |
35 | | - import importlib |
36 | | - if 'Crypto' in sys.modules: |
37 | | - del sys.modules['Crypto'] |
38 | | - if 'Crypto.Cipher' in sys.modules: |
39 | | - del sys.modules['Crypto.Cipher'] |
40 | | - if 'Crypto.Cipher.AES' in sys.modules: |
41 | | - del sys.modules['Crypto.Cipher.AES'] |
42 | | - from Crypto.Cipher import AES |
43 | | - return True |
44 | | - except Exception: |
45 | | - return False |
46 | | - |
47 | | -def derive_key_iv(salt: bytes) -> tuple: |
48 | | - """PBKDF2 派生 key 和 iv,与 dk.ps1/dk 兼容""" |
| 19 | +def _derive_key_iv(salt: bytes) -> tuple: |
| 20 | + """PBKDF2-SHA256 派生 key(32) 和 iv(16)""" |
49 | 21 | derived = hashlib.pbkdf2_hmac('sha256', SALT, salt, ITERATIONS, dklen=48) |
50 | 22 | return derived[:32], derived[32:48] |
51 | 23 |
|
| 24 | +if IS_WINDOWS: |
| 25 | + import ctypes |
| 26 | + from ctypes import wintypes |
| 27 | + |
| 28 | + bcrypt = ctypes.windll.bcrypt |
| 29 | + BCRYPT_AES_ALGORITHM = "AES" |
| 30 | + BCRYPT_CHAIN_MODE_CBC = "ChainingModeCBC" |
| 31 | + BCRYPT_CHAINING_MODE = "ChainingMode" |
| 32 | + |
| 33 | + class AESCipher: |
| 34 | + def __init__(self, key: bytes, iv: bytes): |
| 35 | + self.hAlg = ctypes.c_void_p() |
| 36 | + self.hKey = ctypes.c_void_p() |
| 37 | + self.iv = (ctypes.c_ubyte * len(iv))(*iv) |
| 38 | + bcrypt.BCryptOpenAlgorithmProvider(ctypes.byref(self.hAlg), BCRYPT_AES_ALGORITHM, None, 0) |
| 39 | + mode = BCRYPT_CHAIN_MODE_CBC.encode('utf-16-le') + b'\x00\x00' |
| 40 | + bcrypt.BCryptSetProperty(self.hAlg, BCRYPT_CHAINING_MODE, mode, len(mode), 0) |
| 41 | + bcrypt.BCryptGenerateSymmetricKey(self.hAlg, ctypes.byref(self.hKey), None, 0, key, len(key), 0) |
| 42 | + |
| 43 | + def decrypt(self, ciphertext: bytes) -> bytes: |
| 44 | + out_len = wintypes.ULONG() |
| 45 | + iv_copy = (ctypes.c_ubyte * len(self.iv))(*self.iv) |
| 46 | + bcrypt.BCryptDecrypt(self.hKey, ciphertext, len(ciphertext), None, iv_copy, len(iv_copy), None, 0, ctypes.byref(out_len), 1) |
| 47 | + out_buf = (ctypes.c_ubyte * out_len.value)() |
| 48 | + iv_copy = (ctypes.c_ubyte * len(self.iv))(*self.iv) |
| 49 | + bcrypt.BCryptDecrypt(self.hKey, ciphertext, len(ciphertext), None, iv_copy, len(iv_copy), out_buf, out_len.value, ctypes.byref(out_len), 1) |
| 50 | + return bytes(out_buf[:out_len.value]) |
| 51 | + |
| 52 | + def encrypt(self, plaintext: bytes) -> bytes: |
| 53 | + pad_len = 16 - (len(plaintext) % 16) |
| 54 | + plaintext = plaintext + bytes([pad_len] * pad_len) |
| 55 | + out_len = wintypes.ULONG() |
| 56 | + iv_copy = (ctypes.c_ubyte * len(self.iv))(*self.iv) |
| 57 | + bcrypt.BCryptEncrypt(self.hKey, plaintext, len(plaintext), None, iv_copy, len(iv_copy), None, 0, ctypes.byref(out_len), 0) |
| 58 | + out_buf = (ctypes.c_ubyte * out_len.value)() |
| 59 | + iv_copy = (ctypes.c_ubyte * len(self.iv))(*self.iv) |
| 60 | + bcrypt.BCryptEncrypt(self.hKey, plaintext, len(plaintext), None, iv_copy, len(iv_copy), out_buf, out_len.value, ctypes.byref(out_len), 0) |
| 61 | + return bytes(out_buf[:out_len.value]) |
| 62 | + |
| 63 | + def __del__(self): |
| 64 | + if self.hKey: bcrypt.BCryptDestroyKey(self.hKey) |
| 65 | + if self.hAlg: bcrypt.BCryptCloseAlgorithmProvider(self.hAlg, 0) |
| 66 | + |
52 | 67 | def decrypt_keys(keys_file: str) -> list: |
53 | 68 | """解密 keys.enc 文件,返回 key 列表""" |
54 | 69 | if not os.path.isfile(keys_file): |
55 | 70 | return [] |
56 | | - with open(keys_file, 'rb') as f: |
57 | | - data = f.read() |
58 | | - if len(data) < 17: |
59 | | - return [] |
60 | | - if data[:8] != b'Salted__': |
61 | | - return [] |
62 | | - salt = data[8:16] |
63 | | - ciphertext = data[16:] |
64 | | - key, iv = derive_key_iv(salt) |
65 | 71 | try: |
66 | | - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes |
67 | | - from cryptography.hazmat.primitives import padding |
68 | | - cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) |
69 | | - decryptor = cipher.decryptor() |
70 | | - padded = decryptor.update(ciphertext) + decryptor.finalize() |
71 | | - unpadder = padding.PKCS7(128).unpadder() |
72 | | - plaintext = unpadder.update(padded) + unpadder.finalize() |
73 | | - except ImportError: |
74 | | - # fallback: PyCryptodome |
75 | | - from Crypto.Cipher import AES |
76 | | - from Crypto.Util.Padding import unpad |
77 | | - cipher = AES.new(key, AES.MODE_CBC, iv) |
78 | | - plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size) |
79 | | - text = plaintext.decode('utf-8') |
80 | | - keys = [] |
81 | | - for line in text.split('\n'): |
82 | | - line = line.strip() |
83 | | - if line: |
84 | | - keys.append(line.split('\t')[0]) |
85 | | - return keys |
| 72 | + if IS_WINDOWS: |
| 73 | + with open(keys_file, 'rb') as f: |
| 74 | + data = f.read() |
| 75 | + if len(data) < 17 or data[:8] != b'Salted__': |
| 76 | + return [] |
| 77 | + salt, ciphertext = data[8:16], data[16:] |
| 78 | + key, iv = _derive_key_iv(salt) |
| 79 | + cipher = AESCipher(key, iv) |
| 80 | + text = cipher.decrypt(ciphertext).decode('utf-8') |
| 81 | + else: |
| 82 | + result = subprocess.run( |
| 83 | + ['openssl', 'enc', '-d', '-aes-256-cbc', '-pbkdf2', '-in', keys_file, '-pass', f'pass:{SALT.decode()}'], |
| 84 | + capture_output=True |
| 85 | + ) |
| 86 | + if result.returncode != 0: |
| 87 | + return [] |
| 88 | + text = result.stdout.decode('utf-8') |
| 89 | + keys = [] |
| 90 | + for line in text.split('\n'): |
| 91 | + line = line.strip() |
| 92 | + if line: |
| 93 | + keys.append(line.split('\t')[0]) |
| 94 | + return keys |
| 95 | + except Exception: |
| 96 | + return [] |
86 | 97 |
|
87 | 98 | def encrypt_keys(keys: list, keys_file: str): |
88 | 99 | """加密 key 列表并写入文件""" |
89 | | - salt = secrets.token_bytes(8) |
90 | | - key, iv = derive_key_iv(salt) |
91 | 100 | text = '\n'.join(f"{k}\t" for k in keys) |
92 | | - plaintext = text.encode('utf-8') |
93 | | - try: |
94 | | - from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes |
95 | | - from cryptography.hazmat.primitives import padding |
96 | | - padder = padding.PKCS7(128).padder() |
97 | | - padded = padder.update(plaintext) + padder.finalize() |
98 | | - cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) |
99 | | - encryptor = cipher.encryptor() |
100 | | - ciphertext = encryptor.update(padded) + encryptor.finalize() |
101 | | - except ImportError: |
102 | | - from Crypto.Cipher import AES |
103 | | - from Crypto.Util.Padding import pad |
104 | | - cipher = AES.new(key, AES.MODE_CBC, iv) |
105 | | - ciphertext = cipher.encrypt(pad(plaintext, AES.block_size)) |
106 | | - with open(keys_file, 'wb') as f: |
107 | | - f.write(b'Salted__' + salt + ciphertext) |
| 101 | + if IS_WINDOWS: |
| 102 | + import secrets |
| 103 | + salt = secrets.token_bytes(8) |
| 104 | + key, iv = _derive_key_iv(salt) |
| 105 | + cipher = AESCipher(key, iv) |
| 106 | + ciphertext = cipher.encrypt(text.encode('utf-8')) |
| 107 | + with open(keys_file, 'wb') as f: |
| 108 | + f.write(b'Salted__' + salt + ciphertext) |
| 109 | + else: |
| 110 | + subprocess.run( |
| 111 | + ['openssl', 'enc', '-aes-256-cbc', '-pbkdf2', '-salt', '-out', keys_file, '-pass', f'pass:{SALT.decode()}'], |
| 112 | + input=text.encode('utf-8'), |
| 113 | + check=True |
| 114 | + ) |
108 | 115 |
|
109 | 116 | API_URL = 'https://app.factory.ai/api/organization/members/chat-usage' |
110 | 117 | API_TIMEOUT = 4 |
@@ -619,9 +626,6 @@ def log_message(self, format, *args): |
619 | 626 | pass |
620 | 627 |
|
621 | 628 | def run(port, web_dir, oroio_dir, dk_path): |
622 | | - if not _ensure_crypto(): |
623 | | - print("错误: 无法加载加密库,请手动安装: pip install pycryptodome", file=sys.stderr) |
624 | | - sys.exit(1) |
625 | 629 | os.chdir(web_dir) |
626 | 630 |
|
627 | 631 | handler = lambda *args, **kwargs: OroioHandler( |
|
0 commit comments