From d4c2a5ef05b42ffa8824a4ae9e7dc21361b8b5c3 Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 29 Jul 2026 22:58:24 +0200 Subject: [PATCH 1/2] integrate the decryptor --- lib/findmy_decryptor.py | 521 ++++++++++++++++++++++++++++++++++++++++ lib/log_manager.py | 103 +++++--- pyproject.toml | 18 ++ requirements.txt | 2 +- uv.lock | 148 ++++++++++++ 5 files changed, 759 insertions(+), 33 deletions(-) create mode 100644 lib/findmy_decryptor.py create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/lib/findmy_decryptor.py b/lib/findmy_decryptor.py new file mode 100644 index 0000000..03da3ec --- /dev/null +++ b/lib/findmy_decryptor.py @@ -0,0 +1,521 @@ +#!/usr/bin/env python3 +""" +FindMy Cache Data Decryption Script +FindMy 缓存数据解密脚本 +Based on reverse engineering analysis of FindMyCrypto.framework +基于对 FindMyCrypto.framework 的逆向分析 + +Supports decryption of two cache file groups: +支持两组缓存文件解密: +1. FMIP Group (Find My iPhone) - uses FMIPDataManager.bplist key +1. FMIP 组 (Find My iPhone) - 使用 FMIPDataManager.bplist 密钥 + - SafeLocations.data, Items.data, Devices.data, FamilyMembers.data, ItemGroups.data, Owner.data + - SafeLocations.data, Items.data, Devices.data, FamilyMembers.data, ItemGroups.data, Owner.data +2. FMF Group (Find My Friends) - uses FMFDataManager.bplist key +2. FMF 组 (Find My Friends) - 使用 FMFDataManager.bplist 密钥 + - FriendCacheData.data + - FriendCacheData.data + +Encryption Process: +加密流程: +1. ChaCha20-Poly1305 AEAD symmetric encryption +1. ChaCha20-Poly1305 AEAD 对称加密 +2. Uses pre-stored symmetric key for decryption +2. 使用预先存储的对称密钥进行解密 +""" + +import plistlib +import base64 +import json +import datetime +from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 +import os + + +class FindMyDecryptor: + """ + FindMy Cache Data Decryptor Class + FindMy 缓存数据解密器类 + Handles decryption of FindMy cache files using ChaCha20-Poly1305 encryption + 使用 ChaCha20-Poly1305 加密处理 FindMy 缓存文件解密 + """ + + def __init__(self): + """ + Initialize the decryptor with empty keys + 初始化解密器,设置空密钥 + """ + self.fmip_key = None # FMIP group key / FMIP 组密钥 + self.fmf_key = None # FMF group key / FMF 组密钥 + + def load_keys_from_file(self, file_path, key_type): + """ + Load key data from file + 从文件加载密钥数据 + + Args: + file_path (str): Path to the key file + key_type (str): Type of key ('FMIP' or 'FMF') + + Returns: + bool: True if successful, False otherwise + """ + try: + # Try to load key from file + print(f"🔍 尝试从文件加载 {key_type} 密钥: {file_path}") + + # Try to read plist file + # 尝试读取 plist 文件 + with open(file_path, "rb") as f: + plist_data = plistlib.load(f) + + # Successfully loaded key data from file + print(f"✅ 成功从文件加载 {key_type} 密钥数据") + return self.load_keys_from_plist(plist_data, key_type) + + except FileNotFoundError: + # File not found error + print(f"❌ 文件不存在: {file_path}") + return False + except Exception as e: + # File reading error + print(f"❌ 文件读取错误: {e}") + return False + + def load_keys_from_input(self, key_type, filename): + """ + Load key data from user input + 从用户输入加载密钥数据 + + Args: + key_type (str): Type of key ('FMIP' or 'FMF') + filename (str): Name of the key file for reference + + Returns: + bool: True if successful, False otherwise + """ + try: + # Prompt user for file contents + print(f"\n📝 请输入 {filename} 文件的内容:") + # Hint: you can use 'xxd -p filename | tr -d '\\n'' to get hex content + print(f"提示:可以使用 'xxd -p {filename} | tr -d '\\n'' 获取十六进制内容") + + hex_input = input("请输入十六进制内容: ").strip() + + # Remove spaces and newlines + # 移除空格和换行符 + hex_input = hex_input.replace(" ", "").replace("\n", "") + + # Convert to binary data + # 转换为二进制数据 + binary_data = bytes.fromhex(hex_input) + + # Parse as plist + # 解析为 plist + plist_data = plistlib.loads(binary_data) + + # Successfully parsed user input data + print(f"✅ 成功解析用户输入的 {key_type} 数据") + return self.load_keys_from_plist(plist_data, key_type) + + except ValueError as e: + # Hexadecimal data format error + print(f"❌ 十六进制数据格式错误: {e}") + return False + except Exception as e: + # Data parsing error + print(f"❌ 数据解析错误: {e}") + return False + + def load_keys_from_plist(self, plist_data, key_type): + """ + Load keys from plist data + 从 plist 数据加载密钥 + + Args: + plist_data (dict): Parsed plist data + key_type (str): Type of key ('FMIP' or 'FMF') + + Returns: + bool: True if successful, False otherwise + """ + try: + # Get symmetric key, supports two formats: + # 获取对称密钥,支持两种格式: + # Format 1: Direct base64 string + # 格式1: 直接 base64 字符串 + # Format 2: Nested dictionary structure {'key': {'data': base64_string}} + # 格式2: 嵌套字典结构 {'key': {'data': base64_string}} + + symmetric_key_data = plist_data.get("symmetricKey") + + if not symmetric_key_data: + # Missing symmetricKey in plist + print(f"❌ plist 中缺少 symmetricKey") + return False + + # Check if it's a nested structure + # 检查是否为嵌套结构 + if isinstance(symmetric_key_data, dict): + # Nested format: symmetricKey -> key -> data + # 嵌套格式: symmetricKey -> key -> data + key_dict = symmetric_key_data.get("key", {}) + if isinstance(key_dict, dict): + symmetric_key_b64 = key_dict.get("data") + if isinstance(symmetric_key_b64, bytes): + # If it's bytes type, use directly + # 如果是 bytes 类型,直接使用 + symmetric_key_bytes = symmetric_key_b64 + else: + # If it's string, decode base64 + # 如果是字符串,解码 base64 + symmetric_key_bytes = base64.b64decode(symmetric_key_b64) + else: + # Invalid symmetricKey structure + print(f"❌ 无效的 symmetricKey 结构") + return False + else: + # Direct format: directly base64 string + # 直接格式: 直接是 base64 字符串 + symmetric_key_bytes = base64.b64decode(symmetric_key_data) + + # Print symmetric key length + print(f"🔑 {key_type} 对称密钥长度: {len(symmetric_key_bytes)} 字节") + + if len(symmetric_key_bytes) != 32: + # Incorrect symmetric key length, should be 32 bytes + print(f"❌ {key_type} 对称密钥长度不正确,应为 32 字节") + return False + + # Save to different attributes based on key type + # 根据密钥类型保存到不同的属性 + if key_type == "FMIP": + self.fmip_key = symmetric_key_bytes + elif key_type == "FMF": + self.fmf_key = symmetric_key_bytes + else: + # Unknown key type + print(f"❌ 未知的密钥类型: {key_type}") + return False + + # Symmetric key loaded successfully + print(f"✅ {key_type} 对称密钥加载成功") + return True + + except Exception as e: + # Key loading error + print(f"❌ 密钥加载错误: {e}") + return False + + def decrypt_chacha20_poly1305(self, encrypted_data, key_type): + """ + Decrypt data using ChaCha20-Poly1305 + 使用 ChaCha20-Poly1305 解密数据 + + Args: + encrypted_data (bytes): Encrypted data to decrypt + key_type (str): Type of key to use ('FMIP' or 'FMF') + + Returns: + bytes: Decrypted plaintext data, or None if failed + """ + try: + # Parse encryptedData structure + # 解析 encryptedData 结构 + # First 12 bytes: nonce + # 前12字节: nonce + # Middle part: ciphertext + # 中间部分: 密文 + # Last 16 bytes: authentication tag + # 后16字节: 认证标签 + + if len(encrypted_data) < 28: # At least need nonce + auth_tag + # Insufficient encrypted data length + print("❌ 加密数据长度不足") + return None + + nonce = encrypted_data[:12] + ciphertext_with_tag = encrypted_data[12:] + + print(f"🔍 Nonce: {nonce.hex()}") + # Ciphertext + tag length + print(f"🔍 密文+标签长度: {len(ciphertext_with_tag)} 字节") + + # Select corresponding key based on key type + # 根据密钥类型选择对应的密钥 + if key_type == "FMIP": + symmetric_key = self.fmip_key + elif key_type == "FMF": + symmetric_key = self.fmf_key + else: + # Unknown key type + print(f"❌ 未知的密钥类型: {key_type}") + return None + + # Decrypt using symmetric key + # 使用对称密钥解密 + if symmetric_key is None: + # Symmetric key not initialized + print(f"❌ {key_type} 对称密钥未初始化") + return None + + # Create ChaCha20Poly1305 decryptor + # 创建 ChaCha20Poly1305 解密器 + cipher = ChaCha20Poly1305(symmetric_key) + + # Decrypt data + # 解密数据 + plaintext = cipher.decrypt(nonce, ciphertext_with_tag, None) + # Decryption successful, plaintext length + print(f"✅ 解密成功,明文长度: {len(plaintext)} 字节") + + return plaintext + + except Exception as e: + # ChaCha20-Poly1305 decryption error + print(f"❌ ChaCha20-Poly1305 解密错误: {e}") + return None + + def format_plist_data(self, data, indent=0): + """ + Format plist data for readability + 格式化 plist 数据以便可读 + + Args: + data: Data to format + indent (int): Indentation level + + Returns: + str: Formatted string representation + """ + spaces = " " * indent + + if isinstance(data, dict): + result = "{\n" + for key, value in data.items(): + result += ( + f"{spaces} {key}: {self.format_plist_data(value, indent + 1)}\n" + ) + result += f"{spaces}}}" + return result + elif isinstance(data, list): + result = "[\n" + for item in data: + result += f"{spaces} {self.format_plist_data(item, indent + 1)}\n" + result += f"{spaces}]" + return result + elif isinstance(data, bytes): + return f"<{len(data)} bytes: {data[:20].hex()}{'...' if len(data) > 20 else ''}>" + elif isinstance(data, datetime.datetime): + return f"" + else: + return str(data) + + def decrypt_cache_file(self, file_path, key_type): + """ + Decrypt cache file + 解密缓存文件 + + Args: + file_path (str): Path to the cache file to decrypt + key_type (str): Type of key to use ('FMIP' or 'FMF') + + Returns: + bytes: Decrypted data, or None if failed + """ + try: + # Start decrypting file + print(f"🔍 开始解密文件: {file_path} (使用 {key_type} 密钥)") + + # Read plist file + # 读取 plist 文件 + with open(file_path, "rb") as f: + plist_data = plistlib.load(f) + + # Print plist data structure + print("📋 plist 数据结构:") + for key, value in plist_data.items(): + if isinstance(value, bytes): + print(f" {key}: {len(value)} 字节") + else: + print(f" {key}: {value}") + + # Extract encrypted data + # 提取加密数据 + encrypted_data = plist_data.get("encryptedData") + + if not encrypted_data: + # Missing encryptedData + print("❌ 缺少 encryptedData") + return None + + # Print encryptedData length + print(f"🔍 encryptedData 长度: {len(encrypted_data)} 字节") + + # Decrypt data + # 解密数据 + plaintext = self.decrypt_chacha20_poly1305(encrypted_data, key_type) + + if plaintext: + # Decryption successful! + print(f"✅ 解密成功!") + # First 100 bytes of plaintext + print(f"📝 明文前100字节: {plaintext[:100]}") + + # Try to parse decrypted data + # 尝试解析解密后的数据 + try: + # First check if it's plist format + # 首先检查是否为 plist 格式 + if plaintext.startswith(b"bplist"): + # Decrypted data is plist format, parsing... + print("📊 解密后的数据是 plist 格式,正在解析...") + inner_plist = plistlib.loads(plaintext) + # Content of decrypted plist + print("📋 解密后的 plist 内容:") + print(self.format_plist_data(inner_plist)) + + # Save decrypted data to file + # 保存解密后的数据到文件 + output_file = f"{file_path}.decrypted.plist" + with open(output_file, "wb") as f: + plistlib.dump(inner_plist, f) + # Decrypted data saved to + print(f"💾 解密后的数据已保存到: {output_file}") + + elif plaintext.startswith(b"{"): + # JSON format + # JSON 格式 + json_data = json.loads(plaintext.decode("utf-8")) + # Parsed as JSON format + print("📊 解析为 JSON 格式:") + print(json.dumps(json_data, indent=2, ensure_ascii=False)) + + else: + # Other formats, try to display as text + # 其他格式,尝试作为文本显示 + # Plaintext content (first 1000 bytes) + print("📝 明文内容 (前1000字节):") + try: + print(plaintext[:1000].decode("utf-8")) + except UnicodeDecodeError: + # Binary data + print(f"二进制数据: {plaintext[:1000]}") + + # Save raw decrypted data + # 保存原始解密数据 + output_file = f"{file_path}.decrypted.bin" + with open(output_file, "wb") as f: + f.write(plaintext) + # Raw decrypted data saved to + print(f"💾 原始解密数据已保存到: {output_file}") + + except Exception as e: + # Error occurred while parsing decrypted data + print(f"⚠️ 解析解密数据时出错: {e}") + # Plaintext content (first 1000 bytes) + print("📝 明文内容 (前1000字节):") + print(plaintext[:1000]) + + return plaintext + + return None + + except Exception as e: + # File decryption error + print(f"❌ 解密文件错误: {e}") + return None + + +def main(): + """ + Main function + 主函数 + """ + decryptor = FindMyDecryptor() + + # FindMy Cache Data Decryption Tool + print("🔐 FindMy 缓存数据解密工具") + print("=" * 50) + + # Define key files and corresponding cache files + # 定义密钥文件和对应的缓存文件 + key_configs = [ + { + "key_type": "FMIP", + "key_file": "FMIPDataManager.bplist", + "cache_files": [ + "com.apple.findmy.fmipcore/SafeLocations.data", + "com.apple.findmy.fmipcore/Items.data", + "com.apple.findmy.fmipcore/Devices.data", + "com.apple.findmy.fmipcore/FamilyMembers.data", + "com.apple.findmy.fmipcore/ItemGroups.data", + "com.apple.findmy.fmipcore/Owner.data", + ], + }, + { + "key_type": "FMF", + "key_file": "FMFDataManager.bplist", + "cache_files": ["com.apple.findmy.fmfcore/FriendCacheData.data"], + }, + ] + + # Process each key group + # 处理每个密钥组 + for config in key_configs: + key_type = config["key_type"] + key_file = config["key_file"] + cache_files = config["cache_files"] + + # Processing key group + print(f"\n🔧 处理 {key_type} 组...") + + # Check if there are corresponding cache files to decrypt + # 检查是否有对应的缓存文件需要解密 + existing_files = [f for f in cache_files if os.path.exists(f)] + if not existing_files: + # No cache files found for group, skipping... + print(f"⚠️ 没有找到 {key_type} 组的缓存文件,跳过...") + continue + + # Found cache files for group + print(f"📁 发现 {key_type} 组缓存文件: {existing_files}") + + # Try to load key from file + # 尝试从文件加载密钥 + print(f"1️⃣ 尝试从文件加载 {key_type} 密钥...") + if not decryptor.load_keys_from_file(key_file, key_type): + # Key file loading failed, please manually input key data + print(f"⚠️ {key_type} 密钥文件加载失败,请手动输入密钥数据") + + # Load key from user input + # 从用户输入加载密钥 + print(f"2️⃣ 从用户输入加载 {key_type} 密钥:") + try: + if not decryptor.load_keys_from_input(key_type, key_file): + # Key loading failed, skipping this group... + print(f"❌ {key_type} 密钥加载失败,跳过该组...") + continue + except KeyboardInterrupt: + # User cancelled input, skipping group... + print(f"\n❌ 用户取消输入,跳过 {key_type} 组...") + continue + except Exception as e: + # Input error, skipping group... + print(f"❌ 输入错误: {e},跳过 {key_type} 组...") + continue + + # Decrypt cache files for this group + # 解密该组的缓存文件 + print(f"3️⃣ 开始解密 {key_type} 组缓存文件...") + for file_path in existing_files: + # Decrypt file + print(f"\n📁 解密文件: {file_path}") + decryptor.decrypt_cache_file(file_path, key_type) + + # All files processed! + print("\n🎉 所有文件处理完成!") + + +if __name__ == "__main__": + main() diff --git a/lib/log_manager.py b/lib/log_manager.py index 82aaa2a..ce8bbd7 100644 --- a/lib/log_manager.py +++ b/lib/log_manager.py @@ -5,12 +5,41 @@ from collections import defaultdict from influxdb_client import InfluxDBClient +import plistlib + + +def bytes_to_string(data): + if isinstance(data, bytes): + return data.decode("utf-8", errors="replace") + elif isinstance(data, dict): + return { + bytes_to_string(key): bytes_to_string(value) for key, value in data.items() + } + elif isinstance(data, list): + return [bytes_to_string(item) for item in data] + else: + return data + class LogManager(object): - def __init__(self, findmy_files, store_keys, timestamp_key, log_folder, - name_keys, name_separator, json_layer_separator, null_str, - date_format, no_date_folder, log_location, influx_host, - influx_token, influx_org, influx_bucket): + def __init__( + self, + findmy_files, + store_keys, + timestamp_key, + log_folder, + name_keys, + name_separator, + json_layer_separator, + null_str, + date_format, + no_date_folder, + log_location, + influx_host, + influx_token, + influx_org, + influx_bucket, + ): self._findmy_files = findmy_files self._store_keys = store_keys self._timestamp_key = timestamp_key @@ -22,26 +51,25 @@ def __init__(self, findmy_files, store_keys, timestamp_key, log_folder, # Log location config self._log_location = log_location - if self._log_location == 'local': + if self._log_location == "local": self._log_folder = log_folder self._no_date_folder = no_date_folder - elif self._log_location == 'influx': + elif self._log_location == "influx": self._influx_org = influx_org self._influx_bucket = influx_bucket influx_client = InfluxDBClient( - url=influx_host, - token=influx_token, - org=self._influx_org + url=influx_host, token=influx_token, org=self._influx_org ) self._influx_write_api = influx_client.write_api() else: - raise ValueError(f"Unsupported log location: `{self._log_location}`, supported log locations: `local`, `influx`") + raise ValueError( + f"Unsupported log location: `{self._log_location}`, supported log locations: `local`, `influx`" + ) self._latest_log = {} self._log_cnt = defaultdict(int) - self._keys = sorted(list( - set(self._name_keys).union(set(self._store_keys)))) + self._keys = sorted(list(set(self._name_keys).union(set(self._store_keys)))) def _process_item(self, item): item_dict = {} @@ -61,47 +89,54 @@ def _get_items_dict(self): items_dict = {} for file in self._findmy_files: try: - with open(file, 'r') as f: - json_data = json.loads(f.read()) + with open(file, "rb") as f: + plist_data = plistlib.load(f, fmt=plistlib.FMT_BINARY) + converted_data = bytes_to_string(plist_data) + json_data = json.dumps(converted_data) for item in json_data: item = self._process_item(item) - name = [item[key] if key in item else self._null_str - for key in self._name_keys] + name = [ + item[key] if key in item else self._null_str + for key in self._name_keys + ] name = self._name_separator.join(name) if name in items_dict: - raise ValueError(f'{name} already exists!') + raise ValueError(f"{name} already exists!") items_dict[name] = item except: pass if not items_dict: - raise RuntimeError(f'No devices found. Please check if Full Disk ' - 'Access has been granted to Terminal.') + raise RuntimeError( + f"No devices found. Please check if Full Disk " + "Access has been granted to Terminal." + ) return items_dict def _save_log(self, name, data): """ Routes _save_log() calls to their proper function based on log location """ - if self._log_location == 'local': + if self._log_location == "local": return self._save_log_local(name, data) - elif self._log_location == 'influx': + elif self._log_location == "influx": return self._save_log_influx(name, data) def _save_log_local(self, name, data): log_folder = self._log_folder if not self._no_date_folder: log_folder = os.path.join( - log_folder, datetime.now().strftime(self._date_format)) + log_folder, datetime.now().strftime(self._date_format) + ) if not os.path.exists(log_folder): os.makedirs(log_folder) - path = os.path.join(log_folder, name + '.csv') + path = os.path.join(log_folder, name + ".csv") if not os.path.exists(path): - with open(path, 'w') as f: + with open(path, "w") as f: writer = csv.writer(f) writer.writerow(self._keys) - with open(path, 'a') as f: + with open(path, "a") as f: writer = csv.writer(f) writer.writerow([data[k] for k in self._keys]) @@ -109,7 +144,7 @@ def _save_log_influx(self, name, data): """ Sends log data to an InfluxDB2 database bucket """ - with open("test.txt", 'w') as f: + with open("test.txt", "w") as f: f.write(f"Saving Log Line for {name}: {data}") self._influx_write_api.write( @@ -122,21 +157,25 @@ def _save_log_influx(self, name, data): for key in self._keys if key in data }, - "time": data['location|timeStamp'] + "time": data["location|timeStamp"], }, - write_precision='ms' + write_precision="ms", ) def refresh_log(self): items_dict = self._get_items_dict() for name in items_dict: # On non-local log locations, don't push null data - if self._log_location != 'local' and (items_dict[name]['location|timeStamp'] == 'NULL' or - items_dict[name]['location|longitude'] == 'NULL'): + if self._log_location != "local" and ( + items_dict[name]["location|timeStamp"] == "NULL" + or items_dict[name]["location|longitude"] == "NULL" + ): continue - if (name not in self._latest_log or - self._latest_log[name] != items_dict[name]): + if ( + name not in self._latest_log + or self._latest_log[name] != items_dict[name] + ): self._save_log(name, items_dict[name]) self._latest_log[name] = items_dict[name] self._log_cnt[name] += 1 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..195ff31 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,18 @@ +[project] +name = "findmyhistory" +version = "0.1.0" +description = "FindMyHistory project" +requires-python = ">=3.10" +dependencies = [ + "certifi==2024.2.2", + "influxdb-client==1.42.0", + "python-dateutil==2.9.0.post0", + "reactivex==4.0.4", + "setuptools==70.0.0", + "six==1.16.0", + "tabulate==0.9.0", + "typing_extensions>=4.12.0", + "urllib3==2.2.1", + "wcwidth==0.2.13", + "wheel==0.43.0", +] diff --git a/requirements.txt b/requirements.txt index 24de119..0bc9b33 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ reactivex==4.0.4 setuptools==70.0.0 six==1.16.0 tabulate==0.9.0 -typing_extensions==4.11.0 +typing_extensions==4.12.0 urllib3==2.2.1 wcwidth==0.2.13 wheel==0.43.0 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..d443622 --- /dev/null +++ b/uv.lock @@ -0,0 +1,148 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "certifi" +version = "2024.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/da/e94e26401b62acd6d91df2b52954aceb7f561743aa5ccc32152886c76c96/certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f", size = 164886, upload-time = "2024-02-02T01:22:17.364Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1", size = 163774, upload-time = "2024-02-02T01:22:14.86Z" }, +] + +[[package]] +name = "findmyhistory" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "certifi" }, + { name = "influxdb-client" }, + { name = "python-dateutil" }, + { name = "reactivex" }, + { name = "setuptools" }, + { name = "six" }, + { name = "tabulate" }, + { name = "typing-extensions" }, + { name = "urllib3" }, + { name = "wcwidth" }, + { name = "wheel" }, +] + +[package.metadata] +requires-dist = [ + { name = "certifi", specifier = "==2024.2.2" }, + { name = "influxdb-client", specifier = "==1.42.0" }, + { name = "python-dateutil", specifier = "==2.9.0.post0" }, + { name = "reactivex", specifier = "==4.0.4" }, + { name = "setuptools", specifier = "==70.0.0" }, + { name = "six", specifier = "==1.16.0" }, + { name = "tabulate", specifier = "==0.9.0" }, + { name = "typing-extensions", specifier = ">=4.12.0" }, + { name = "urllib3", specifier = "==2.2.1" }, + { name = "wcwidth", specifier = "==0.2.13" }, + { name = "wheel", specifier = "==0.43.0" }, +] + +[[package]] +name = "influxdb-client" +version = "1.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "python-dateutil" }, + { name = "reactivex" }, + { name = "setuptools" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/84/e8746501977f66fe7546d721971d2cc268a30762abab1f91ab73bd95163d/influxdb_client-1.42.0.tar.gz", hash = "sha256:f5e877feb671eda41e2b5c98ed1dc8ec3327fd8991360dc614822119cda06491", size = 382354, upload-time = "2024-04-17T06:05:29.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/f7/eaa6664f12134e3b94c69ef070d26d2efbb7885dbe8f1b0e4ccfbec06d05/influxdb_client-1.42.0-py3-none-any.whl", hash = "sha256:0161b963f221d5c1769202f41ff55f5d79e00e6dc24e2a0729c82a1e131956ee", size = 744598, upload-time = "2024-04-17T06:05:26.87Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "reactivex" +version = "4.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/63/f776322df4d7b456446eff78c4e64f14c3c26d57d46b4e06c18807d5d99c/reactivex-4.0.4.tar.gz", hash = "sha256:e912e6591022ab9176df8348a653fe8c8fa7a301f26f9931c9d8c78a650e04e8", size = 119177, upload-time = "2022-07-16T07:11:53.689Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/3f/2ed8c1b8fe3fc2ed816ba40554ef703aad8c51700e2606c139fcf9b7f791/reactivex-4.0.4-py3-none-any.whl", hash = "sha256:0004796c420bd9e68aad8e65627d85a8e13f293de76656165dffbcb3a0e3fb6a", size = 217791, upload-time = "2022-07-16T07:11:52.061Z" }, +] + +[[package]] +name = "setuptools" +version = "70.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/60/5db2249526c9b453c5bb8b9f6965fcab0ddb7f40ad734420b3b421f7da44/setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0", size = 2265182, upload-time = "2024-05-21T10:28:18.891Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/88/70c5767a0e43eb4451c2200f07d042a4bcd7639276003a9c54a68cfcc1f8/setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4", size = 863432, upload-time = "2024-05-21T10:28:12.781Z" }, +] + +[[package]] +name = "six" +version = "1.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/71/39/171f1c67cd00715f190ba0b100d606d440a28c93c7714febeca8b79af85e/six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", size = 34041, upload-time = "2021-05-05T14:18:18.379Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/5a/e7c31adbe875f2abbb91bd84cf2dc52d792b5a01506781dbcf25c91daf11/six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254", size = 11053, upload-time = "2021-05-05T14:18:17.237Z" }, +] + +[[package]] +name = "tabulate" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "urllib3" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7a/50/7fd50a27caa0652cd4caf224aa87741ea41d3265ad13f010886167cfcc79/urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19", size = 291020, upload-time = "2024-02-18T03:55:57.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/73/a68704750a7679d0b6d3ad7aa8d4da8e14e151ae82e6fee774e6e0d05ec8/urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", size = 121067, upload-time = "2024-02-18T03:55:54.704Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, +] + +[[package]] +name = "wheel" +version = "0.43.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/d6/ac9cd92ea2ad502ff7c1ab683806a9deb34711a1e2bd8a59814e8fc27e69/wheel-0.43.0.tar.gz", hash = "sha256:465ef92c69fa5c5da2d1cf8ac40559a8c940886afcef87dcf14b9470862f1d85", size = 99109, upload-time = "2024-03-11T19:29:17.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7d/cd/d7460c9a869b16c3dd4e1e403cce337df165368c71d6af229a74699622ce/wheel-0.43.0-py3-none-any.whl", hash = "sha256:55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81", size = 65775, upload-time = "2024-03-11T19:29:15.522Z" }, +] From 4702e72854c76c1811ad59102e04a0df16d0aa9d Mon Sep 17 00:00:00 2001 From: brandon Date: Thu, 30 Jul 2026 13:28:56 +0200 Subject: [PATCH 2/2] better error handling and decryption information --- .vscode/launch.json | 15 ++ lib/findmy_decryptor.py | 495 ++++++++++++++++++++++++++-------------- lib/log_manager.py | 135 +++++++++-- main.py | 21 +- pyproject.toml | 1 + uv.lock | 178 +++++++++++++++ 6 files changed, 657 insertions(+), 188 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..6b76b4f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/lib/findmy_decryptor.py b/lib/findmy_decryptor.py index 03da3ec..0bf6389 100644 --- a/lib/findmy_decryptor.py +++ b/lib/findmy_decryptor.py @@ -32,6 +32,25 @@ import os +def _log(en, zh, *args, **kwargs): + """Print a log line with both English and Chinese versions. + + Print a log line in both English and Chinese so output is readable for either audience. + + Args: + en: English message (may contain %-style placeholders matching *args). + zh: Chinese message (may contain %-style placeholders matching *args). + *args: Positional values substituted into both messages. + **kwargs: Forwarded to ``print`` (e.g. ``end``). + """ + if args: + en_msg = en % args if "%" in en else en.format(*args) + zh_msg = zh % args if "%" in zh else zh.format(*args) + else: + en_msg, zh_msg = en, zh + print(f"[EN] {en_msg} | [ZH] {zh_msg}", **kwargs) + + class FindMyDecryptor: """ FindMy Cache Data Decryptor Class @@ -61,25 +80,32 @@ def load_keys_from_file(self, file_path, key_type): bool: True if successful, False otherwise """ try: - # Try to load key from file - print(f"🔍 尝试从文件加载 {key_type} 密钥: {file_path}") - - # Try to read plist file - # 尝试读取 plist 文件 + # Try to load key from file / 尝试从文件加载密钥 + _log( + "🔍 Trying to load %s key from file: %s", + "🔍 尝试从文件加载 %s 密钥: %s", + key_type, file_path, + ) + + # Try to read plist file / 尝试读取 plist 文件 with open(file_path, "rb") as f: plist_data = plistlib.load(f) - # Successfully loaded key data from file - print(f"✅ 成功从文件加载 {key_type} 密钥数据") + # Successfully loaded key data from file / 成功从文件加载密钥数据 + _log( + "✅ Successfully loaded %s key data from file", + "✅ 成功从文件加载 %s 密钥数据", + key_type, + ) return self.load_keys_from_plist(plist_data, key_type) except FileNotFoundError: - # File not found error - print(f"❌ 文件不存在: {file_path}") + # File not found error / 文件不存在错误 + _log("❌ File does not exist: %s", "❌ 文件不存在: %s", file_path) return False except Exception as e: - # File reading error - print(f"❌ 文件读取错误: {e}") + # File reading error / 文件读取错误 + _log("❌ File read error: %s", "❌ 文件读取错误: %s", e) return False def load_keys_from_input(self, key_type, filename): @@ -95,36 +121,52 @@ def load_keys_from_input(self, key_type, filename): bool: True if successful, False otherwise """ try: - # Prompt user for file contents - print(f"\n📝 请输入 {filename} 文件的内容:") - # Hint: you can use 'xxd -p filename | tr -d '\\n'' to get hex content - print(f"提示:可以使用 'xxd -p {filename} | tr -d '\\n'' 获取十六进制内容") - - hex_input = input("请输入十六进制内容: ").strip() - - # Remove spaces and newlines - # 移除空格和换行符 + # Prompt user for file contents / 提示用户输入文件内容 + _log( + "\n📝 Please enter the contents of the %s file:", + "\n📝 请输入 %s 文件的内容:", + filename, end="\n", + ) + # Hint: you can use 'xxd -p filename | tr -d "\n"' to get hex content + # 提示:可以使用 'xxd -p filename | tr -d "\n"' 获取十六进制内容 + _log( + "Hint: you can use 'xxd -p %s | tr -d \"\\n\"' to get hex content", + "提示:可以使用 'xxd -p %s | tr -d \"\\n\"' 获取十六进制内容", + filename, + ) + + hex_input = input( + "[EN] Please enter hex content: | [ZH] 请输入十六进制内容: " + ).strip() + + # Remove spaces and newlines / 移除空格和换行符 hex_input = hex_input.replace(" ", "").replace("\n", "") - # Convert to binary data - # 转换为二进制数据 + # Convert to binary data / 转换为二进制数据 binary_data = bytes.fromhex(hex_input) - # Parse as plist - # 解析为 plist + # Parse as plist / 解析为 plist plist_data = plistlib.loads(binary_data) - # Successfully parsed user input data - print(f"✅ 成功解析用户输入的 {key_type} 数据") + # Successfully parsed user input data / 成功解析用户输入的数据 + _log( + "✅ Successfully parsed %s data from user input", + "✅ 成功解析用户输入的 %s 数据", + key_type, + ) return self.load_keys_from_plist(plist_data, key_type) except ValueError as e: - # Hexadecimal data format error - print(f"❌ 十六进制数据格式错误: {e}") + # Hexadecimal data format error / 十六进制数据格式错误 + _log( + "❌ Hexadecimal data format error: %s", + "❌ 十六进制数据格式错误: %s", + e, + ) return False except Exception as e: - # Data parsing error - print(f"❌ 数据解析错误: {e}") + # Data parsing error / 数据解析错误 + _log("❌ Data parsing error: %s", "❌ 数据解析错误: %s", e) return False def load_keys_from_plist(self, plist_data, key_type): @@ -142,20 +184,21 @@ def load_keys_from_plist(self, plist_data, key_type): try: # Get symmetric key, supports two formats: # 获取对称密钥,支持两种格式: - # Format 1: Direct base64 string - # 格式1: 直接 base64 字符串 + # Format 1: Direct base64 string / 格式1: 直接 base64 字符串 # Format 2: Nested dictionary structure {'key': {'data': base64_string}} # 格式2: 嵌套字典结构 {'key': {'data': base64_string}} symmetric_key_data = plist_data.get("symmetricKey") if not symmetric_key_data: - # Missing symmetricKey in plist - print(f"❌ plist 中缺少 symmetricKey") + # Missing symmetricKey in plist / plist 中缺少 symmetricKey + _log( + "❌ Missing symmetricKey in plist", + "❌ plist 中缺少 symmetricKey", + ) return False - # Check if it's a nested structure - # 检查是否为嵌套结构 + # Check if it's a nested structure / 检查是否为嵌套结构 if isinstance(symmetric_key_data, dict): # Nested format: symmetricKey -> key -> data # 嵌套格式: symmetricKey -> key -> data @@ -163,48 +206,64 @@ def load_keys_from_plist(self, plist_data, key_type): if isinstance(key_dict, dict): symmetric_key_b64 = key_dict.get("data") if isinstance(symmetric_key_b64, bytes): - # If it's bytes type, use directly - # 如果是 bytes 类型,直接使用 + # If it's bytes type, use directly / 如果是 bytes 类型,直接使用 symmetric_key_bytes = symmetric_key_b64 else: - # If it's string, decode base64 - # 如果是字符串,解码 base64 + # If it's string, decode base64 / 如果是字符串,解码 base64 symmetric_key_bytes = base64.b64decode(symmetric_key_b64) else: - # Invalid symmetricKey structure - print(f"❌ 无效的 symmetricKey 结构") + # Invalid symmetricKey structure / 无效的 symmetricKey 结构 + _log( + "❌ Invalid symmetricKey structure", + "❌ 无效的 symmetricKey 结构", + ) return False else: - # Direct format: directly base64 string - # 直接格式: 直接是 base64 字符串 + # Direct format: directly base64 string / 直接格式: 直接是 base64 字符串 symmetric_key_bytes = base64.b64decode(symmetric_key_data) - # Print symmetric key length - print(f"🔑 {key_type} 对称密钥长度: {len(symmetric_key_bytes)} 字节") + # Print symmetric key length / 打印对称密钥长度 + _log( + "🔑 %s symmetric key length: %d bytes", + "🔑 %s 对称密钥长度: %d 字节", + key_type, len(symmetric_key_bytes), + ) if len(symmetric_key_bytes) != 32: # Incorrect symmetric key length, should be 32 bytes - print(f"❌ {key_type} 对称密钥长度不正确,应为 32 字节") + # 对称密钥长度不正确,应为 32 字节 + _log( + "❌ %s symmetric key length is incorrect, should be 32 bytes", + "❌ %s 对称密钥长度不正确,应为 32 字节", + key_type, + ) return False - # Save to different attributes based on key type - # 根据密钥类型保存到不同的属性 + # Save to different attributes based on key type / 根据密钥类型保存到不同的属性 if key_type == "FMIP": self.fmip_key = symmetric_key_bytes elif key_type == "FMF": self.fmf_key = symmetric_key_bytes else: - # Unknown key type - print(f"❌ 未知的密钥类型: {key_type}") + # Unknown key type / 未知的密钥类型 + _log( + "❌ Unknown key type: %s", + "❌ 未知的密钥类型: %s", + key_type, + ) return False - # Symmetric key loaded successfully - print(f"✅ {key_type} 对称密钥加载成功") + # Symmetric key loaded successfully / 对称密钥加载成功 + _log( + "✅ %s symmetric key loaded successfully", + "✅ %s 对称密钥加载成功", + key_type, + ) return True except Exception as e: - # Key loading error - print(f"❌ 密钥加载错误: {e}") + # Key loading error / 密钥加载错误 + _log("❌ Key loading error: %s", "❌ 密钥加载错误: %s", e) return False def decrypt_chacha20_poly1305(self, encrypted_data, key_type): @@ -220,60 +279,75 @@ def decrypt_chacha20_poly1305(self, encrypted_data, key_type): bytes: Decrypted plaintext data, or None if failed """ try: - # Parse encryptedData structure - # 解析 encryptedData 结构 - # First 12 bytes: nonce - # 前12字节: nonce - # Middle part: ciphertext - # 中间部分: 密文 - # Last 16 bytes: authentication tag - # 后16字节: 认证标签 + # Parse encryptedData structure / 解析 encryptedData 结构 + # First 12 bytes: nonce / 前12字节: nonce + # Middle part: ciphertext / 中间部分: 密文 + # Last 16 bytes: authentication tag / 后16字节: 认证标签 if len(encrypted_data) < 28: # At least need nonce + auth_tag - # Insufficient encrypted data length - print("❌ 加密数据长度不足") + # Insufficient encrypted data length / 加密数据长度不足 + _log( + "❌ Encrypted data length is insufficient", + "❌ 加密数据长度不足", + ) return None nonce = encrypted_data[:12] ciphertext_with_tag = encrypted_data[12:] - print(f"🔍 Nonce: {nonce.hex()}") - # Ciphertext + tag length - print(f"🔍 密文+标签长度: {len(ciphertext_with_tag)} 字节") + # Nonce and ciphertext+tag length / Nonce 与 密文+标签长度 + _log("🔍 Nonce: %s", "🔍 Nonce: %s", nonce.hex()) + _log( + "🔍 Ciphertext + tag length: %d bytes", + "🔍 密文+标签长度: %d 字节", + len(ciphertext_with_tag), + ) - # Select corresponding key based on key type - # 根据密钥类型选择对应的密钥 + # Select corresponding key based on key type / 根据密钥类型选择对应的密钥 if key_type == "FMIP": symmetric_key = self.fmip_key elif key_type == "FMF": symmetric_key = self.fmf_key else: - # Unknown key type - print(f"❌ 未知的密钥类型: {key_type}") + # Unknown key type / 未知的密钥类型 + _log( + "❌ Unknown key type: %s", + "❌ 未知的密钥类型: %s", + key_type, + ) return None - # Decrypt using symmetric key - # 使用对称密钥解密 + # Decrypt using symmetric key / 使用对称密钥解密 if symmetric_key is None: - # Symmetric key not initialized - print(f"❌ {key_type} 对称密钥未初始化") + # Symmetric key not initialized / 对称密钥未初始化 + _log( + "❌ %s symmetric key is not initialized", + "❌ %s 对称密钥未初始化", + key_type, + ) return None - # Create ChaCha20Poly1305 decryptor - # 创建 ChaCha20Poly1305 解密器 + # Create ChaCha20Poly1305 decryptor / 创建 ChaCha20Poly1305 解密器 cipher = ChaCha20Poly1305(symmetric_key) - # Decrypt data - # 解密数据 + # Decrypt data / 解密数据 plaintext = cipher.decrypt(nonce, ciphertext_with_tag, None) - # Decryption successful, plaintext length - print(f"✅ 解密成功,明文长度: {len(plaintext)} 字节") + # Decryption successful, plaintext length / 解密成功,明文长度 + _log( + "✅ Decryption successful, plaintext length: %d bytes", + "✅ 解密成功,明文长度: %d 字节", + len(plaintext), + ) return plaintext except Exception as e: - # ChaCha20-Poly1305 decryption error - print(f"❌ ChaCha20-Poly1305 解密错误: {e}") + # ChaCha20-Poly1305 decryption error / ChaCha20-Poly1305 解密错误 + _log( + "❌ ChaCha20-Poly1305 decryption error: %s", + "❌ ChaCha20-Poly1305 解密错误: %s", + e, + ) return None def format_plist_data(self, data, indent=0): @@ -324,97 +398,133 @@ def decrypt_cache_file(self, file_path, key_type): bytes: Decrypted data, or None if failed """ try: - # Start decrypting file - print(f"🔍 开始解密文件: {file_path} (使用 {key_type} 密钥)") - - # Read plist file - # 读取 plist 文件 + # Start decrypting file / 开始解密文件 + _log( + "🔍 Starting to decrypt file: %s (using %s key)", + "🔍 开始解密文件: %s (使用 %s 密钥)", + file_path, key_type, + ) + + # Read plist file / 读取 plist 文件 with open(file_path, "rb") as f: plist_data = plistlib.load(f) - # Print plist data structure - print("📋 plist 数据结构:") + # Print plist data structure / 打印 plist 数据结构 + _log("📋 plist data structure:", "📋 plist 数据结构:") for key, value in plist_data.items(): if isinstance(value, bytes): - print(f" {key}: {len(value)} 字节") + _log(" %s: %d bytes", " %s: %d 字节", key, len(value)) else: - print(f" {key}: {value}") + _log(" %s: %s", " %s: %s", key, value) - # Extract encrypted data - # 提取加密数据 + # Extract encrypted data / 提取加密数据 encrypted_data = plist_data.get("encryptedData") if not encrypted_data: - # Missing encryptedData - print("❌ 缺少 encryptedData") + # Missing encryptedData / 缺少 encryptedData + _log("❌ Missing encryptedData", "❌ 缺少 encryptedData") return None - # Print encryptedData length - print(f"🔍 encryptedData 长度: {len(encrypted_data)} 字节") + # Print encryptedData length / 打印 encryptedData 长度 + _log( + "🔍 encryptedData length: %d bytes", + "🔍 encryptedData 长度: %d 字节", + len(encrypted_data), + ) - # Decrypt data - # 解密数据 + # Decrypt data / 解密数据 plaintext = self.decrypt_chacha20_poly1305(encrypted_data, key_type) if plaintext: - # Decryption successful! - print(f"✅ 解密成功!") - # First 100 bytes of plaintext - print(f"📝 明文前100字节: {plaintext[:100]}") + # Decryption successful! / 解密成功! + _log("✅ Decryption successful!", "✅ 解密成功!") + # First 100 bytes of plaintext / 明文前100字节 + _log( + "📝 First 100 bytes of plaintext: %s", + "📝 明文前100字节: %s", + plaintext[:100], + ) - # Try to parse decrypted data - # 尝试解析解密后的数据 + # Try to parse decrypted data / 尝试解析解密后的数据 try: - # First check if it's plist format - # 首先检查是否为 plist 格式 + # First check if it's plist format / 首先检查是否为 plist 格式 if plaintext.startswith(b"bplist"): # Decrypted data is plist format, parsing... - print("📊 解密后的数据是 plist 格式,正在解析...") + # 解密后的数据是 plist 格式,正在解析... + _log( + "📊 Decrypted data is in plist format, parsing...", + "📊 解密后的数据是 plist 格式,正在解析...", + ) inner_plist = plistlib.loads(plaintext) - # Content of decrypted plist - print("📋 解密后的 plist 内容:") + # Content of decrypted plist / 解密后的 plist 内容 + _log( + "📋 Decrypted plist content:", + "📋 解密后的 plist 内容:", + ) print(self.format_plist_data(inner_plist)) - # Save decrypted data to file - # 保存解密后的数据到文件 + # Save decrypted data to file / 保存解密后的数据到文件 output_file = f"{file_path}.decrypted.plist" with open(output_file, "wb") as f: plistlib.dump(inner_plist, f) - # Decrypted data saved to - print(f"💾 解密后的数据已保存到: {output_file}") + # Decrypted data saved to / 解密后的数据已保存到 + _log( + "💾 Decrypted data saved to: %s", + "💾 解密后的数据已保存到: %s", + output_file, + ) elif plaintext.startswith(b"{"): - # JSON format - # JSON 格式 + # JSON format / JSON 格式 json_data = json.loads(plaintext.decode("utf-8")) - # Parsed as JSON format - print("📊 解析为 JSON 格式:") + # Parsed as JSON format / 解析为 JSON 格式 + _log( + "📊 Parsed as JSON format:", + "📊 解析为 JSON 格式:", + ) print(json.dumps(json_data, indent=2, ensure_ascii=False)) else: # Other formats, try to display as text # 其他格式,尝试作为文本显示 - # Plaintext content (first 1000 bytes) - print("📝 明文内容 (前1000字节):") + # Plaintext content (first 1000 bytes) / 明文内容 (前1000字节) + _log( + "📝 Plaintext content (first 1000 bytes):", + "📝 明文内容 (前1000字节):", + ) try: print(plaintext[:1000].decode("utf-8")) except UnicodeDecodeError: - # Binary data - print(f"二进制数据: {plaintext[:1000]}") - - # Save raw decrypted data - # 保存原始解密数据 + # Binary data / 二进制数据 + _log( + "Binary data: %s", + "二进制数据: %s", + plaintext[:1000], + ) + + # Save raw decrypted data / 保存原始解密数据 output_file = f"{file_path}.decrypted.bin" with open(output_file, "wb") as f: f.write(plaintext) - # Raw decrypted data saved to - print(f"💾 原始解密数据已保存到: {output_file}") + # Raw decrypted data saved to / 原始解密数据已保存到 + _log( + "💾 Raw decrypted data saved to: %s", + "💾 原始解密数据已保存到: %s", + output_file, + ) except Exception as e: - # Error occurred while parsing decrypted data - print(f"⚠️ 解析解密数据时出错: {e}") - # Plaintext content (first 1000 bytes) - print("📝 明文内容 (前1000字节):") + # Error occurred while parsing decrypted data / 解析解密数据时出错 + _log( + "⚠️ Error while parsing decrypted data: %s", + "⚠️ 解析解密数据时出错: %s", + e, + ) + # Plaintext content (first 1000 bytes) / 明文内容 (前1000字节) + _log( + "📝 Plaintext content (first 1000 bytes):", + "📝 明文内容 (前1000字节):", + ) print(plaintext[:1000]) return plaintext @@ -422,8 +532,12 @@ def decrypt_cache_file(self, file_path, key_type): return None except Exception as e: - # File decryption error - print(f"❌ 解密文件错误: {e}") + # File decryption error / 解密文件错误 + _log( + "❌ File decryption error: %s", + "❌ 解密文件错误: %s", + e, + ) return None @@ -434,12 +548,14 @@ def main(): """ decryptor = FindMyDecryptor() - # FindMy Cache Data Decryption Tool - print("🔐 FindMy 缓存数据解密工具") + # FindMy Cache Data Decryption Tool / FindMy 缓存数据解密工具 + _log( + "🔐 FindMy Cache Data Decryption Tool", + "🔐 FindMy 缓存数据解密工具", + ) print("=" * 50) - # Define key files and corresponding cache files - # 定义密钥文件和对应的缓存文件 + # Define key files and corresponding cache files / 定义密钥文件和对应的缓存文件 key_configs = [ { "key_type": "FMIP", @@ -460,61 +576,102 @@ def main(): }, ] - # Process each key group - # 处理每个密钥组 + # Process each key group / 处理每个密钥组 for config in key_configs: key_type = config["key_type"] key_file = config["key_file"] cache_files = config["cache_files"] - # Processing key group - print(f"\n🔧 处理 {key_type} 组...") + # Processing key group / 正在处理密钥组 + _log( + "\n🔧 Processing %s group...", + "\n🔧 处理 %s 组...", + key_type, end="\n", + ) # Check if there are corresponding cache files to decrypt # 检查是否有对应的缓存文件需要解密 existing_files = [f for f in cache_files if os.path.exists(f)] if not existing_files: - # No cache files found for group, skipping... - print(f"⚠️ 没有找到 {key_type} 组的缓存文件,跳过...") + # No cache files found for group, skipping... / 没有找到缓存文件,跳过... + _log( + "⚠️ No cache files found for %s group, skipping...", + "⚠️ 没有找到 %s 组的缓存文件,跳过...", + key_type, + ) continue - # Found cache files for group - print(f"📁 发现 {key_type} 组缓存文件: {existing_files}") - - # Try to load key from file - # 尝试从文件加载密钥 - print(f"1️⃣ 尝试从文件加载 {key_type} 密钥...") + # Found cache files for group / 发现密钥组对应的缓存文件 + _log( + "📁 Found %s group cache files: %s", + "📁 发现 %s 组缓存文件: %s", + key_type, existing_files, + ) + + # Try to load key from file / 尝试从文件加载密钥 + _log( + "1️⃣ Trying to load %s key from file...", + "1️⃣ 尝试从文件加载 %s 密钥...", + key_type, + ) if not decryptor.load_keys_from_file(key_file, key_type): # Key file loading failed, please manually input key data - print(f"⚠️ {key_type} 密钥文件加载失败,请手动输入密钥数据") - - # Load key from user input - # 从用户输入加载密钥 - print(f"2️⃣ 从用户输入加载 {key_type} 密钥:") + # 密钥文件加载失败,请手动输入密钥数据 + _log( + "⚠️ %s key file loading failed, please manually input key data", + "⚠️ %s 密钥文件加载失败,请手动输入密钥数据", + key_type, + ) + + # Load key from user input / 从用户输入加载密钥 + _log( + "2️⃣ Loading %s key from user input:", + "2️⃣ 从用户输入加载 %s 密钥:", + key_type, + ) try: if not decryptor.load_keys_from_input(key_type, key_file): - # Key loading failed, skipping this group... - print(f"❌ {key_type} 密钥加载失败,跳过该组...") + # Key loading failed, skipping this group... / 密钥加载失败,跳过该组... + _log( + "❌ %s key loading failed, skipping this group...", + "❌ %s 密钥加载失败,跳过该组...", + key_type, + ) continue except KeyboardInterrupt: - # User cancelled input, skipping group... - print(f"\n❌ 用户取消输入,跳过 {key_type} 组...") + # User cancelled input, skipping group... / 用户取消输入,跳过该组... + _log( + "\n❌ User cancelled input, skipping %s group...", + "\n❌ 用户取消输入,跳过 %s 组...", + key_type, + ) continue except Exception as e: - # Input error, skipping group... - print(f"❌ 输入错误: {e},跳过 {key_type} 组...") + # Input error, skipping group... / 输入错误,跳过该组... + _log( + "❌ Input error: %s, skipping %s group...", + "❌ 输入错误: %s,跳过 %s 组...", + e, key_type, + ) continue - # Decrypt cache files for this group - # 解密该组的缓存文件 - print(f"3️⃣ 开始解密 {key_type} 组缓存文件...") + # Decrypt cache files for this group / 解密该组的缓存文件 + _log( + "3️⃣ Starting to decrypt %s group cache files...", + "3️⃣ 开始解密 %s 组缓存文件...", + key_type, + ) for file_path in existing_files: - # Decrypt file - print(f"\n📁 解密文件: {file_path}") + # Decrypt file / 解密文件 + _log( + "\n📁 Decrypting file: %s", + "\n📁 解密文件: %s", + file_path, end="\n", + ) decryptor.decrypt_cache_file(file_path, key_type) - # All files processed! - print("\n🎉 所有文件处理完成!") + # All files processed! / 所有文件处理完成! + _log("\n🎉 All files processed!", "\n🎉 所有文件处理完成!") if __name__ == "__main__": diff --git a/lib/log_manager.py b/lib/log_manager.py index ce8bbd7..9c787e8 100644 --- a/lib/log_manager.py +++ b/lib/log_manager.py @@ -39,6 +39,7 @@ def __init__( influx_token, influx_org, influx_bucket, + decryptor=None, ): self._findmy_files = findmy_files self._store_keys = store_keys @@ -48,8 +49,11 @@ def __init__( self._json_layer_separator = json_layer_separator self._null_str = null_str self._date_format = date_format + self._decryptor = decryptor + self._decrypt_cache = {} # Log location config + # 日志位置配置 self._log_location = log_location if self._log_location == "local": self._log_folder = log_folder @@ -71,6 +75,62 @@ def __init__( self._keys = sorted(list(set(self._name_keys).union(set(self._store_keys)))) + def _key_type_for_file(self, file_path): + """ + Infer the decryptor key type from the file path. + 根据文件路径推断解密器密钥类型。 + """ + # FMIP group paths / FMIP 组路径 + if "fmipcore" in file_path: + return "FMIP" + # FMF group paths / FMF 组路径 + if "fmfcore" in file_path: + return "FMF" + return None + + def _load_and_decrypt(self, file): + """ + Load a FindMy cache file, decrypting it if a decryptor is configured. + 加载 FindMy 缓存文件,如果配置了解密器则进行解密。 + + Results are memoized per (file, mtime) to avoid re-decrypting on + every refresh tick when the file has not changed. + 结果按 (file, mtime) 进行记忆化,避免文件未变更时每次刷新都重新解密。 + """ + if self._decryptor is None: + with open(file, "rb") as f: + return plistlib.load(f, fmt=plistlib.FMT_BINARY) + + try: + mtime = os.path.getmtime(file) + except OSError: + mtime = None + cache_key = (file, mtime) + if cache_key in self._decrypt_cache: + return self._decrypt_cache[cache_key] + + with open(file, "rb") as f: + outer_plist = plistlib.load(f, fmt=plistlib.FMT_BINARY) + + key_type = self._key_type_for_file(file) + if key_type and "encryptedData" in outer_plist: + plaintext = self._decryptor.decrypt_chacha20_poly1305( + outer_plist["encryptedData"], key_type + ) + if plaintext is None: + raise RuntimeError( + f"[EN] Failed to decrypt {file} with {key_type} key. " + "Check that the correct key file was provided. | " + f"[ZH] 使用 {key_type} 密钥解密 {file} 失败。" + "请检查是否提供了正确的密钥文件。" + ) + data = plistlib.loads(plaintext, fmt=plistlib.FMT_BINARY) + else: + data = outer_plist + + self._decrypt_cache[cache_key] = data + return data + def _process_item(self, item): item_dict = {} for key in self._keys: @@ -87,28 +147,66 @@ def _process_item(self, item): def _get_items_dict(self): items_dict = {} + file_errors = {} for file in self._findmy_files: try: - with open(file, "rb") as f: - plist_data = plistlib.load(f, fmt=plistlib.FMT_BINARY) - converted_data = bytes_to_string(plist_data) - json_data = json.dumps(converted_data) - for item in json_data: - item = self._process_item(item) - name = [ - item[key] if key in item else self._null_str - for key in self._name_keys - ] - name = self._name_separator.join(name) - if name in items_dict: - raise ValueError(f"{name} already exists!") - items_dict[name] = item - except: - pass + plist_data = self._load_and_decrypt(file) + except FileNotFoundError: + file_errors[file] = ( + "[EN] File not found. | [ZH] 文件不存在。" + ) + continue + except PermissionError as e: + file_errors[file] = ( + f"[EN] Permission denied ({e}). Grant Full Disk Access " + "to Terminal in System Settings > Privacy & Security. | " + f"[ZH] 没有访问权限({e})。请在“系统设置 > 隐私与安全性”" + "中为终端授予“完全磁盘访问权限”。" + ) + continue + except RuntimeError: + # Raised by _load_and_decrypt on decryption failure. + # Already bilingual; re-raise so the caller surfaces the + # exact cause instead of the generic "No devices found" error. + raise + except plistlib.InvalidFileException as e: + file_errors[file] = ( + f"[EN] Invalid plist file ({e}). | " + f"[ZH] 无效的 plist 文件({e})。" + ) + continue + try: + converted_data = bytes_to_string(plist_data) + json_data = json.dumps(converted_data) + except (TypeError, ValueError) as e: + file_errors[file] = ( + f"[EN] Failed to serialize plist to JSON ({e}). | " + f"[ZH] 将 plist 序列化为 JSON 失败({e})。" + ) + continue + for item in json_data: + item = self._process_item(item) + name = [ + item[key] if key in item else self._null_str + for key in self._name_keys + ] + name = self._name_separator.join(name) + if name in items_dict: + raise ValueError(f"{name} already exists!") + items_dict[name] = item if not items_dict: + if file_errors: + details = " | ".join( + f"{os.path.basename(f)}: {msg}" for f, msg in file_errors.items() + ) + raise RuntimeError( + f"[EN] No devices found. Per-file errors -> {details} | " + f"[ZH] 未找到任何设备。各文件错误 -> {details}" + ) raise RuntimeError( - f"No devices found. Please check if Full Disk " - "Access has been granted to Terminal." + f"[EN] No devices found. Please check if Full Disk " + "Access has been granted to Terminal. | " + "[ZH] 未找到任何设备。请检查是否已为终端授予“完全磁盘访问权限”。" ) return items_dict @@ -166,6 +264,7 @@ def refresh_log(self): items_dict = self._get_items_dict() for name in items_dict: # On non-local log locations, don't push null data + # 在非本地日志位置时,不要推送空值数据 if self._log_location != "local" and ( items_dict[name]["location|timeStamp"] == "NULL" or items_dict[name]["location|longitude"] == "NULL" diff --git a/main.py b/main.py index 3641b0a..0099438 100644 --- a/main.py +++ b/main.py @@ -14,6 +14,7 @@ from lib.constants import TIME_FORMAT from lib.constants import DATE_FORMAT from lib.log_manager import LogManager +from lib.findmy_decryptor import FindMyDecryptor def parse_args(): @@ -79,6 +80,18 @@ def parse_args(): default='local', help='Location to log findmy data. Default: local' ) + parser.add_argument( + '--fmip_key_file', + type=str, + action='store', + default=None, + help='Path to FMIPDataManager.bplist (symmetric key for FMIP cache).') + parser.add_argument( + '--fmf_key_file', + type=str, + action='store', + default=None, + help='Path to FMFDataManager.bplist (symmetric key for FMF cache).') # Influx-specific args parser.add_argument( '--influx_host', @@ -125,6 +138,11 @@ def parse_args(): def main(stdscr, args): stdscr.clear() args = parse_args() + decryptor = FindMyDecryptor() + if args.fmip_key_file: + decryptor.load_keys_from_file(args.fmip_key_file, "FMIP") + if args.fmf_key_file: + decryptor.load_keys_from_file(args.fmf_key_file, "FMF") log_manager = LogManager( findmy_files=[os.path.expanduser(f) for f in FINDMY_FILES], store_keys=args.store_keys, @@ -140,7 +158,8 @@ def main(stdscr, args): influx_host=args.influx_host, influx_token=args.influx_token, influx_org=args.influx_org, - influx_bucket=args.influx_bucket) + influx_bucket=args.influx_bucket, + decryptor=decryptor) while True: log_manager.refresh_log() latest_log, log_cnt = log_manager.get_latest_log() diff --git a/pyproject.toml b/pyproject.toml index 195ff31..9225073 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "FindMyHistory project" requires-python = ">=3.10" dependencies = [ "certifi==2024.2.2", + "cryptography>=49.0.0", "influxdb-client==1.42.0", "python-dateutil==2.9.0.post0", "reactivex==4.0.4", diff --git a/uv.lock b/uv.lock index d443622..7b77ab8 100644 --- a/uv.lock +++ b/uv.lock @@ -11,12 +11,180 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/06/a07f096c664aeb9f01624f858c3add0a4e913d6c96257acb4fce61e7de14/certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1", size = 163774, upload-time = "2024-02-02T01:22:14.86Z" }, ] +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/e9/6d7724983b3d5a0908dbf74f64038ade77c18646ff6636ec7894fd392ce1/cffi-2.1.0-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:b65f590ef2a44640f9a05dbb548a429b4ade77913ce683ac8b1480777658a6c0", size = 183837, upload-time = "2026-07-06T21:32:09.655Z" }, + { url = "https://files.pythonhosted.org/packages/69/aa/24580a278de21fd7322635556334d9b535f1cbc00b0a3919447cdf464c65/cffi-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:164bff1657b2a74f0b6d54e11c9b375bc97b931f2ca9c43fcf875838da1570dd", size = 184226, upload-time = "2026-07-06T21:32:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/88/a9/02cae418ec4beb282ace11958d9d4737793439d561fadc7e6d56f2e2b354/cffi-2.1.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:c941bb58d5a6e1c3892d86e42927ed6c180302f07e6d395d08c416e594b98b46", size = 211107, upload-time = "2026-07-06T21:32:12.328Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/c806937ed5e4c2c7ac30d9d6b76b5dc57ff8b75d83800d9bb11a8253cf2a/cffi-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a016194dbe13d14ee9556e734b772d8d67b947092b268d757fd4290e3ba2dfc2", size = 218733, upload-time = "2026-07-06T21:32:13.67Z" }, + { url = "https://files.pythonhosted.org/packages/f9/cf/398272b8bbfd58aa314fda5a7f1cdbb26d1d78ae324a11211521315dd1f0/cffi-2.1.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03e9810d18c646077e501f661b682fbf5dee4676048527ca3cffe66faa9960dd", size = 205543, upload-time = "2026-07-06T21:32:15.148Z" }, + { url = "https://files.pythonhosted.org/packages/45/ca/f91641185cdd90c36d317a9dc7f85e88ef8682d8b300977baff5e23c35d8/cffi-2.1.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:19c54ac121cad98450b4896fa9a43ee0180d57bc4bc911a33db6cab1efab6cd3", size = 205460, upload-time = "2026-07-06T21:32:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/38/66/04781a77b411f0bb5b234d62c1814754ab75ebe455ccff1b08e8d7aae98f/cffi-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d433a51f1870e43a13b6732f92aaf540ff77c2015097c78556f75a2d6c030e0", size = 218760, upload-time = "2026-07-06T21:32:17.98Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9a/bb1d5ed9c3fcae158e9f6391bf309c95d98c2ac37ed56573228471d0af5e/cffi-2.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d7f118b5adbfdfead90c25822690b02bc8074fba949bb7858bec4ebd55adb43", size = 221230, upload-time = "2026-07-06T21:32:19.407Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/3c1409cdd26094efacd1c36c66e0a6eb9d4296e4fd4f9901b8b2042f4323/cffi-2.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c5f5df567f6eb216de69be06ce55c8b714090fae02b18a3b40da8163b8c5fa9c", size = 213524, upload-time = "2026-07-06T21:32:20.828Z" }, + { url = "https://files.pythonhosted.org/packages/fa/75/74dfb7c3fc6ebbd408038476bd4c1d7e925c62614e7b9c534ecc34218288/cffi-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:11b3fb55f4f8ad92274ed26705f65d8f91457de71f5380061eb6d125a768fecd", size = 220341, upload-time = "2026-07-06T21:32:21.9Z" }, + { url = "https://files.pythonhosted.org/packages/70/b6/9003c33a3e7d2c1306f5962e646457dcfe5a8cd8fce6bbe02d7af25db783/cffi-2.1.0-cp310-cp310-win32.whl", hash = "sha256:9d72af0cf10a76a600a9690078fe31c63b9588c8e86bf9fd353f713c84b5db0f", size = 174578, upload-time = "2026-07-06T21:32:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/8a/26/710688310447531c7a22f857c7f79d9855ec18b03e04494ced723fb37e2f/cffi-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb62edb5bb52cca65fab91a63afa7561607120d26090a7e8fda6fb9f064726da", size = 185071, upload-time = "2026-07-06T21:32:24.671Z" }, + { url = "https://files.pythonhosted.org/packages/d3/67/85c89a59ba36a671e79638f44d466749f08179266a57e4f2ffdf92174072/cffi-2.1.0-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:02cb7ff33ded4f1532476731f89ede53e2e488a8e6205515a82144246ffa7dcc", size = 183845, upload-time = "2026-07-06T21:32:26.32Z" }, + { url = "https://files.pythonhosted.org/packages/ea/dd/e3b0baa2d3d6a857ac72b7efbf18e32e487c9cdafcc13049ad765495b15e/cffi-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5bce581e6b8c235e566a14768a943b172ada3ed73537bb0c0be1edee312d4e7", size = 184186, upload-time = "2026-07-06T21:32:28.025Z" }, + { url = "https://files.pythonhosted.org/packages/65/68/9f3ef890cf3c6ab97bd531c5677f67613d302165d16f8142b2811782a614/cffi-2.1.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:30b65779d598c370374fefabf138d456fd6f3216bfa7bedfab1ba82025b0cd93", size = 211892, upload-time = "2026-07-06T21:32:29.565Z" }, + { url = "https://files.pythonhosted.org/packages/22/d7/1a74539db16d8bfd839ff1515948948efbb162e574650fd3d846896eea95/cffi-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88023dfe18799507b73f1dbb0d14326a17465de1bc9c9c7655c22845e9ddc3a2", size = 218793, upload-time = "2026-07-06T21:32:30.951Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d1/9a5b7169499e8e8d8e636de70b97ac7c9447104d2ff1a2cd94790cea5162/cffi-2.1.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0a96b74cda968eebbad56d973efe5098974f0a9fb323865bf99ea1fd24e3e64c", size = 205737, upload-time = "2026-07-06T21:32:32.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b0/e131a9c41f10607926278453d9596163594fe1c4ebc46efe3b5e5b34eb84/cffi-2.1.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a5781494d4d400a3f47f8f1da94b324f6e6b440a53387774002890a2a2f4b50f", size = 204909, upload-time = "2026-07-06T21:32:33.655Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d2/4398416cd699b35167947c6e22aca52c47e69ad5695073c9f1f2c52e04aa/cffi-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aa7a1b53a2a4452ada2d1b5dade9960b2522f1e61293a811a077439e39029565", size = 217883, upload-time = "2026-07-06T21:32:35.173Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a5/d4fe77b589e5e82d43ebc809bf2e6474afe8e48e32ea050b9357645b6471/cffi-2.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d8272c0e483b024e1b9ad029821470ed8ec65631dbd90217469da0e7cd89f1c", size = 221251, upload-time = "2026-07-06T21:32:36.527Z" }, + { url = "https://files.pythonhosted.org/packages/22/f0/a2fc43084c0433caf7f461bccc013e28f848d04ee1c5ed7fce71423cf4d9/cffi-2.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7762faa47e8ff7eb80bd261d9a7d8eea2d8baa69de5e95b70c1f338bbe712f02", size = 214250, upload-time = "2026-07-06T21:32:37.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/8c/b925975448cf20634a9fbd5efceb807219db452653648d2897c0989cab2d/cffi-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:89095c1968b4ba8285840e131bf2891b09ae137fe2146905acae0354fbce1b5e", size = 219441, upload-time = "2026-07-06T21:32:39.146Z" }, + { url = "https://files.pythonhosted.org/packages/eb/da/5c4918a2d61d86fa927d716cb3d8e4626ef8dc8f605a599d32f33897f59a/cffi-2.1.0-cp311-cp311-win32.whl", hash = "sha256:64c753a0f87a256020004f37a1c8c02c480e725f910f0b2a0f3f07debd1b2479", size = 174496, upload-time = "2026-07-06T21:32:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c8/6c2de1d55cf35ef8b92885d5ef280790f0fb9634d87ea1cc315176aecd61/cffi-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:4f26194e3d95e06501b942642855aed4f953d55e95d7d01b7c4483db3ecff458", size = 185113, upload-time = "2026-07-06T21:32:41.761Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/e8d7cb5783f1841a3c8fb3a7735838d7484d08ec08c9f984b14cac1ac0e9/cffi-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:35aaea0c7ee0e58a5cd8c2fd1a48fdf7ece0d2699b7ecdda08194e9ce5dd9b3d", size = 179927, upload-time = "2026-07-06T21:32:42.961Z" }, + { url = "https://files.pythonhosted.org/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://files.pythonhosted.org/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://files.pythonhosted.org/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://files.pythonhosted.org/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://files.pythonhosted.org/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://files.pythonhosted.org/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://files.pythonhosted.org/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://files.pythonhosted.org/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://files.pythonhosted.org/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://files.pythonhosted.org/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://files.pythonhosted.org/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://files.pythonhosted.org/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://files.pythonhosted.org/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://files.pythonhosted.org/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://files.pythonhosted.org/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://files.pythonhosted.org/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://files.pythonhosted.org/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://files.pythonhosted.org/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://files.pythonhosted.org/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://files.pythonhosted.org/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://files.pythonhosted.org/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://files.pythonhosted.org/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://files.pythonhosted.org/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://files.pythonhosted.org/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://files.pythonhosted.org/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://files.pythonhosted.org/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://files.pythonhosted.org/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://files.pythonhosted.org/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://files.pythonhosted.org/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://files.pythonhosted.org/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://files.pythonhosted.org/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://files.pythonhosted.org/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://files.pythonhosted.org/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://files.pythonhosted.org/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://files.pythonhosted.org/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://files.pythonhosted.org/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://files.pythonhosted.org/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://files.pythonhosted.org/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://files.pythonhosted.org/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" }, + { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" }, + { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" }, + { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" }, +] + [[package]] name = "findmyhistory" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "certifi" }, + { name = "cryptography" }, { name = "influxdb-client" }, { name = "python-dateutil" }, { name = "reactivex" }, @@ -32,6 +200,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "certifi", specifier = "==2024.2.2" }, + { name = "cryptography", specifier = ">=49.0.0" }, { name = "influxdb-client", specifier = "==1.42.0" }, { name = "python-dateutil", specifier = "==2.9.0.post0" }, { name = "reactivex", specifier = "==4.0.4" }, @@ -60,6 +229,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/f7/eaa6664f12134e3b94c69ef070d26d2efbb7885dbe8f1b0e4ccfbec06d05/influxdb_client-1.42.0-py3-none-any.whl", hash = "sha256:0161b963f221d5c1769202f41ff55f5d79e00e6dc24e2a0729c82a1e131956ee", size = 744598, upload-time = "2024-04-17T06:05:26.87Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"