#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 云笔记应用程序 支持本地和云端文件管理,支持文本文件(.txt)和加密笔记(.bjb) 完整修复版 - 支持端到端加密,云端下载的单独密钥文件需用户提供.key文件 """ import sys import os import json import tempfile import traceback from datetime import datetime from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * import requests # 导入加密模块 from DVT_RFSA import * # ==================== 调试日志配置 ==================== DEBUG = True # True=记录日志, False=不记录日志 def debug_log(msg, level="INFO"): """调试日志函数""" if DEBUG: timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3] print(f"[{timestamp}] [{level}] {msg}") sys.stdout.flush() def debug_error(msg): """错误日志(带堆栈)""" if DEBUG: debug_log(msg, "ERROR") traceback.print_exc() # ==================== 配置 ==================== API_BASE_URL = "https://noteapi.dvssvc.site/api" APP_NAME = "CloudNote" def get_app_dir(): """获取程序所在目录""" if getattr(sys, 'frozen', False): return os.path.dirname(sys.executable) else: return os.path.dirname(os.path.abspath(__file__)) APP_DIR = get_app_dir() USER_KEY_DIR = os.path.expanduser(f"~/.{APP_NAME.lower()}/keys") APP_KEY_DIR = os.path.join(APP_DIR, ".keys") if DEBUG: debug_log(f"程序目录: {APP_DIR}") debug_log(f"用户密钥目录: {USER_KEY_DIR}") debug_log(f"程序密钥目录: {APP_KEY_DIR}") KEY_FILES = { 'rsa_private': "global_rsa_private.pem", 'rsa_public': "global_rsa_public.pem", 'x25519_private': "global_x25519_private.bin", 'x25519_public': "global_x25519_public.bin" } def get_key_path(key_name, prefer_user=True): """获取密钥文件路径,优先使用用户目录""" if prefer_user: search_dirs = [USER_KEY_DIR, APP_KEY_DIR] dir_names = ['user', 'app'] else: search_dirs = [APP_KEY_DIR, USER_KEY_DIR] dir_names = ['app', 'user'] for dir_path, dir_type in zip(search_dirs, dir_names): file_path = os.path.join(dir_path, key_name) if os.path.exists(file_path): if DEBUG: debug_log(f"找到密钥 {key_name} 在 {dir_type} 目录: {file_path}") return file_path, dir_type if DEBUG: debug_log(f"密钥 {key_name} 未找到,将使用用户目录路径") return os.path.join(USER_KEY_DIR, key_name), 'user' def ensure_directory(dir_path): """确保目录存在""" try: os.makedirs(dir_path, exist_ok=True) if DEBUG: debug_log(f"目录已确保: {dir_path}") return True except Exception as e: debug_error(f"创建目录失败 {dir_path}: {e}") return False # ==================== 全局密钥管理(双路径) ==================== def ensure_global_keys(): """确保全局密钥对存在,支持双路径存储""" if DEBUG: debug_log("开始检查/生成全局密钥(双路径模式)...") ensure_directory(USER_KEY_DIR) ensure_directory(APP_KEY_DIR) key_pairs = [ ('rsa', KEY_FILES['rsa_private'], KEY_FILES['rsa_public'], generate_rsa_key, 4096), ('x25519', KEY_FILES['x25519_private'], KEY_FILES['x25519_public'], generate_x25519_keys, None) ] for key_type, priv_file, pub_file, gen_func, key_size in key_pairs: user_priv = os.path.join(USER_KEY_DIR, priv_file) user_pub = os.path.join(USER_KEY_DIR, pub_file) user_exists = os.path.exists(user_priv) and os.path.exists(user_pub) app_priv = os.path.join(APP_KEY_DIR, priv_file) app_pub = os.path.join(APP_KEY_DIR, pub_file) app_exists = os.path.exists(app_priv) and os.path.exists(app_pub) if DEBUG: debug_log(f"{key_type.upper()}密钥对 - 用户目录存在: {user_exists}, 程序目录存在: {app_exists}") if not user_exists: if DEBUG: debug_log(f"开始生成{key_type.upper()}密钥对到用户目录...") try: if key_size: priv, pub = gen_func(key_size) else: priv, pub = gen_func() with open(user_priv, 'wb') as f: f.write(priv) with open(user_pub, 'wb') as f: f.write(pub) try: with open(app_priv, 'wb') as f: f.write(priv) with open(app_pub, 'wb') as f: f.write(pub) if DEBUG: debug_log(f"{key_type.upper()}密钥备份到程序目录成功") except Exception as e: if DEBUG: debug_log(f"备份到程序目录失败: {e}") except Exception as e: debug_error(f"{key_type.upper()}密钥生成失败: {e}") raise elif user_exists and not app_exists: try: with open(user_priv, 'rb') as f: priv = f.read() with open(user_pub, 'rb') as f: pub = f.read() with open(app_priv, 'wb') as f: f.write(priv) with open(app_pub, 'wb') as f: f.write(pub) if DEBUG: debug_log(f"{key_type.upper()}密钥备份到程序目录成功") except Exception as e: if DEBUG: debug_log(f"备份失败: {e}") if DEBUG: debug_log("全局密钥检查/生成完成") def get_global_rsa_private(): """获取全局RSA私钥""" key_path, _ = get_key_path(KEY_FILES['rsa_private']) if not os.path.exists(key_path): ensure_global_keys() key_path, _ = get_key_path(KEY_FILES['rsa_private']) with open(key_path, 'rb') as f: return f.read() def get_global_rsa_public(): """获取全局RSA公钥""" key_path, _ = get_key_path(KEY_FILES['rsa_public']) if not os.path.exists(key_path): ensure_global_keys() key_path, _ = get_key_path(KEY_FILES['rsa_public']) with open(key_path, 'rb') as f: return f.read() def get_global_x25519_private(): """获取全局X25519私钥""" key_path, _ = get_key_path(KEY_FILES['x25519_private']) if not os.path.exists(key_path): ensure_global_keys() key_path, _ = get_key_path(KEY_FILES['x25519_private']) with open(key_path, 'rb') as f: return f.read() def get_global_x25519_public(): """获取全局X25519公钥""" key_path, _ = get_key_path(KEY_FILES['x25519_public']) if not os.path.exists(key_path): ensure_global_keys() key_path, _ = get_key_path(KEY_FILES['x25519_public']) with open(key_path, 'rb') as f: return f.read() # ==================== 加密笔记处理 ==================== class EncryptedNoteHandler: """加密笔记处理器 - 核心加密解密逻辑""" @staticmethod def check_file_has_password(file_path): """检查加密文件是否有密码保护""" try: with open(file_path, 'rb') as f: data = f.read() clean_data = remove_security_info(data) json_str = clean_data.decode('utf-8') info = json.loads(json_str) return info.get('has_password', False), info.get('mode', 'aes_rsa'), info.get('single_key', False) except Exception as e: debug_error(f"检查文件密码状态失败: {e}") return False, 'unknown', False @staticmethod def get_file_metadata(file_path): """获取加密文件的元数据(不进行解密)""" try: with open(file_path, 'rb') as f: data = f.read() clean_data = remove_security_info(data) json_str = clean_data.decode('utf-8') info = json.loads(json_str) return { 'has_password': info.get('has_password', False), 'mode': info.get('mode', 'aes_rsa'), 'single_key': info.get('single_key', False), 'algorithm': info.get('algorithm', 'unknown'), 'timestamp': info.get('timestamp', 'unknown') } except Exception as e: debug_error(f"获取文件元数据失败: {e}") return None @staticmethod def encrypt_content(content: str, algorithm: str, key_mode: str, password: str = None, single_private=None, single_public=None, strict_password=True): """ 加密内容 content: 要加密的文本内容 algorithm: 'rsa' 或 'x25519' key_mode: 'global' 或 'single' password: 用户密码(可选) single_private/single_public: 单独密钥对 strict_password: 是否使用高强度密码要求 """ if DEBUG: debug_log(f"开始加密 - 算法: {algorithm}, 密钥模式: {key_mode}, 密码保护: {password is not None}, 严格模式: {strict_password}") debug_log(f"内容长度: {len(content)} 字符") data = content.encode('utf-8') private_key = None public_key = None try: if algorithm == 'rsa': if key_mode == 'global': pub_pem = get_global_rsa_public() if password: encrypted = encrypt_text_aes_rsa_with_password(data, pub_pem, password, add_header=True, strict_password=strict_password) else: encrypted = encrypt_text_aes_rsa(data, pub_pem, add_header=True, strict_password=strict_password) private_key = None public_key = None else: if single_private is None or single_public is None: private_key, public_key = generate_rsa_key(4096) else: private_key = single_private public_key = single_public if password: encrypted = encrypt_text_aes_rsa_with_password(data, public_key, password, add_header=True, strict_password=strict_password) else: encrypted = encrypt_text_aes_rsa(data, public_key, add_header=True, strict_password=strict_password) # 添加single_key标记 json_str = remove_security_info(encrypted).decode('utf-8') info = json.loads(json_str) info['single_key'] = True encrypted = add_security_info(json.dumps(info).encode('utf-8'), 'aes_rsa', bool(password)) return encrypted, private_key, public_key else: # x25519 if key_mode == 'global': pub_raw = get_global_x25519_public() if password: encrypted = encrypt_text_aes_x25519(data, pub_raw, password, add_header=True, strict_password=strict_password) else: encrypted = encrypt_text_aes_x25519(data, pub_raw, add_header=True, strict_password=strict_password) private_key = None public_key = None else: if single_private is None or single_public is None: private_key, public_key = generate_x25519_keys() else: private_key = single_private public_key = single_public if password: encrypted = encrypt_text_aes_x25519(data, public_key, password, add_header=True, strict_password=strict_password) else: encrypted = encrypt_text_aes_x25519(data, public_key, add_header=True, strict_password=strict_password) # 添加single_key标记 json_str = remove_security_info(encrypted).decode('utf-8') info = json.loads(json_str) info['single_key'] = True encrypted = add_security_info(json.dumps(info).encode('utf-8'), 'aes_x25519', bool(password)) return encrypted, private_key, public_key return encrypted, private_key, public_key except Exception as e: debug_error(f"加密过程异常: {e}") raise Exception(f"加密失败: {str(e)}") @staticmethod def decrypt_file(file_path, password: str = None, single_private: bytes = None): """ 解密文件 file_path: 文件路径 password: 用户密码(可选) single_private: 单独私钥(可选) """ if DEBUG: debug_log(f"开始解密文件: {file_path}") debug_log(f"密码提供: {password is not None}, 单独私钥提供: {single_private is not None}") if password: debug_log(f"密码长度: {len(password)}") try: with open(file_path, 'rb') as f: data = f.read() clean_data = remove_security_info(data) json_str = clean_data.decode('utf-8') info = json.loads(json_str) mode = info.get('mode', 'aes_rsa') has_password = info.get('has_password', False) single_key = info.get('single_key', False) if DEBUG: debug_log(f"加密信息 - 模式: {mode}, 有密码: {has_password}, 单文件密钥: {single_key}") if mode in ['aes_rsa', 'aes_rsa_password_protected']: if single_key: if single_private is None: raise ValueError("单独密钥_需要选择密钥文件") priv = single_private else: priv = get_global_rsa_private() if has_password: if password is None: raise ValueError("密码保护_需要输入密码") decrypted = decrypt_text_aes_rsa_with_password(clean_data, priv, password, has_header=False) else: decrypted = decrypt_text_aes_rsa(clean_data, priv, has_header=False) elif mode in ['aes_x25519', 'aes_x25519_password_protected']: if single_key: if single_private is None: raise ValueError("单独密钥_需要选择密钥文件") priv = single_private else: priv = get_global_x25519_private() if has_password: if password is None: raise ValueError("密码保护_需要输入密码") decrypted = decrypt_text_aes_x25519(clean_data, priv, password, has_header=False) else: decrypted = decrypt_text_aes_x25519(clean_data, priv, has_header=False) else: raise ValueError(f"未知加密模式: {mode}") # 解密结果可能是 bytes 或 str,统一转为 bytes if isinstance(decrypted, str): decrypted = decrypted.encode('utf-8') if DEBUG: debug_log(f"解密成功,解密后数据大小: {len(decrypted)}字节") return decrypted except Exception as e: debug_error(f"解密过程异常: {e}") raise # ==================== 通用解密函数(修复版:支持单独密钥文件选择) ==================== def decrypt_bjb_file(file_path, parent_window, is_cloud_file=False, cloud_filename=None): """ 解密.bjb文件的通用函数 file_path: 文件路径 parent_window: 父窗口(用于弹窗) is_cloud_file: 是否为云端下载的文件 cloud_filename: 云端原始文件名(用于提示) 返回: (content, success) """ if DEBUG: debug_log(f"通用解密函数: {file_path}, 云端文件: {is_cloud_file}") display_name = cloud_filename if cloud_filename else os.path.basename(file_path) try: # 首先获取文件元数据,判断是否需要单独密钥 metadata = EncryptedNoteHandler.get_file_metadata(file_path) if metadata is None: QMessageBox.critical(parent_window, "文件错误", "无法读取文件元数据,文件可能已损坏") return None, False has_password = metadata['has_password'] single_key = metadata['single_key'] mode = metadata['mode'] if DEBUG: debug_log(f"文件元数据 - has_password: {has_password}, single_key: {single_key}, mode: {mode}") single_private = None # ========== 处理单独密钥模式 ========== if single_key: if DEBUG: debug_log("检测到单独密钥模式,需要查找或选择.key文件") # 首先尝试在相同目录下查找对应的.key文件 key_file_path = file_path + '.key' if os.path.exists(key_file_path): if DEBUG: debug_log(f"找到本地密钥文件: {key_file_path}") try: with open(key_file_path, 'rb') as f: single_private = f.read() if DEBUG: debug_log(f"密钥文件读取成功,长度: {len(single_private)}字节") except Exception as e: debug_error(f"读取密钥文件失败: {e}") QMessageBox.critical(parent_window, "读取失败", f"无法读取密钥文件: {str(e)}") return None, False else: # 没有找到,弹出文件选择对话框 if DEBUG: debug_log("未找到本地密钥文件,弹出选择对话框") # 构建提示信息 msg = f"文件「{display_name}」使用单独密钥加密(端到端加密)。\n\n" msg += "请选择对应的私钥文件(.key):\n" msg += "提示:此文件加密时会在同目录生成同名.key文件。" if is_cloud_file: msg = f"云端文件「{display_name}」使用单独密钥加密(端到端加密)。\n\n" msg += "服务器不存储您的私钥,请选择您本地保存的对应.key文件:\n" msg += "提示:创建此文件时会在本地生成同名的.key文件。" key_file_path, ok = QFileDialog.getOpenFileName( parent_window, "选择密钥文件 - " + display_name, "", "密钥文件 (*.key);;所有文件 (*.*)" ) if not ok or not key_file_path: QMessageBox.warning(parent_window, "密钥缺失", f"无法解密「{display_name}」:缺少对应的私钥文件\n\n" "此文件是使用【安全加密】模式创建的,\n" "需要当初保存时生成的.key文件才能解密。\n" "请找到对应的.key文件后重试。") return None, False try: with open(key_file_path, 'rb') as f: single_private = f.read() if DEBUG: debug_log(f"用户选择密钥文件: {key_file_path}, 长度: {len(single_private)}字节") except Exception as e: debug_error(f"读取用户选择的密钥文件失败: {e}") QMessageBox.critical(parent_window, "读取失败", f"无法读取密钥文件: {str(e)}") return None, False # ========== 处理密码保护模式 ========== if has_password: if DEBUG: debug_log("检测到密码保护,弹出密码输入框") max_attempts = 3 attempts = 0 success = False while attempts < max_attempts and not success: pwd, ok = QInputDialog.getText( parent_window, "输入密码 - " + display_name, f"文件「{display_name}」受密码保护\n请输入密码 (尝试 {attempts + 1}/{max_attempts}):", QLineEdit.Password ) if not ok: if DEBUG: debug_log("用户取消输入密码") return None, False if pwd: try: if DEBUG: debug_log(f"尝试密码解密 (长度: {len(pwd)})") decrypted = EncryptedNoteHandler.decrypt_file( file_path, password=pwd, single_private=single_private ) if isinstance(decrypted, bytes): content = decrypted.decode('utf-8') else: content = str(decrypted) if DEBUG: debug_log("密码解密成功") return content, True except ValueError as e: error_msg = str(e) if DEBUG: debug_log(f"密码解密失败: {error_msg}") if "单独密钥" in error_msg: QMessageBox.warning(parent_window, "密钥错误", "解密失败:密钥文件不匹配\n请选择正确的.key文件") return None, False attempts += 1 if attempts < max_attempts: QMessageBox.warning(parent_window, "密码错误", f"密码错误,还剩 {max_attempts - attempts} 次尝试") else: QMessageBox.warning(parent_window, "解密失败", "多次密码错误,无法解密文件") return None, False except Exception as e: error_msg = str(e) if DEBUG: debug_log(f"解密异常: {error_msg}") attempts += 1 if attempts < max_attempts: QMessageBox.warning(parent_window, "解密失败", f"解密失败: {error_msg}\n还剩 {max_attempts - attempts} 次尝试") else: QMessageBox.warning(parent_window, "解密失败", f"多次尝试失败,无法解密文件\n错误: {error_msg}") return None, False else: attempts += 1 QMessageBox.warning(parent_window, "密码为空", "密码不能为空") return None, False else: # ========== 无密码保护,直接解密 ========== if DEBUG: debug_log("无密码保护,直接解密") try: decrypted = EncryptedNoteHandler.decrypt_file( file_path, password=None, single_private=single_private ) if isinstance(decrypted, bytes): content = decrypted.decode('utf-8') else: content = str(decrypted) return content, True except ValueError as e: error_msg = str(e) if "单独密钥" in error_msg: QMessageBox.warning(parent_window, "密钥错误", "解密失败:需要对应的.key密钥文件\n请选择正确的.key文件") else: QMessageBox.warning(parent_window, "解密失败", str(e)) return None, False except Exception as e: debug_error(f"解密失败: {e}") QMessageBox.critical(parent_window, "解密失败", str(e)) return None, False except Exception as e: debug_error(f"解密过程异常: {e}") QMessageBox.critical(parent_window, "解密失败", str(e)) return None, False # ==================== 云端API客户端 ==================== class CloudClient: """云端API客户端,处理所有网络请求""" def __init__(self): if DEBUG: debug_log("初始化CloudClient") self.token = None self.user_id = None self.username = None def set_auth(self, token, user_id, username): """设置认证信息""" if DEBUG: debug_log(f"设置认证信息 - 用户: {username}, ID: {user_id}") self.token = token self.user_id = user_id self.username = username def clear_auth(self): """清除认证信息""" if DEBUG: debug_log("清除认证信息") self.token = None self.user_id = None self.username = None def _headers(self): """获取请求头""" if self.token: return {'x-access-token': self.token} return {} def _request(self, method, endpoint, **kwargs): """发送HTTP请求""" url = f"{API_BASE_URL}{endpoint}" headers = self._headers() if 'headers' in kwargs: headers.update(kwargs.pop('headers')) try: resp = requests.request(method, url, headers=headers, timeout=30, **kwargs) if resp.status_code == 401: return None return resp except requests.exceptions.ConnectionError: QMessageBox.critical(None, "网络错误", "无法连接到服务器,请检查网络连接") return None except Exception as e: QMessageBox.critical(None, "网络错误", f"请求失败: {str(e)}") return None def register(self, username, email, password): """用户注册""" resp = self._request('POST', '/register', json={'username': username, 'email': email, 'password': password}) if resp and resp.status_code == 201: return resp.json() return None def login(self, email, password): """用户登录""" resp = self._request('POST', '/login', json={'email': email, 'password': password}) if resp and resp.status_code == 200: data = resp.json() self.set_auth(data['token'], data['user_id'], data['username']) return data return None def get_filetree(self, folder='/'): """获取文件树""" resp = self._request('GET', '/filetree', params={'folder': folder}) if resp and resp.status_code == 200: return resp.json() return None def create_folder(self, name, parent='/'): """创建文件夹""" resp = self._request('POST', '/create_folder', json={'name': name, 'parent': parent}) if resp and resp.status_code == 201: return resp.json() return None def upload_file(self, file_path, parent_folder='/'): """上传文件""" ext = os.path.splitext(file_path)[1].lower() if ext not in ['.txt', '.bjb']: QMessageBox.warning(None, "错误", "只支持上传 .txt 和 .bjb 文件") return None with open(file_path, 'rb') as f: files = {'file': (os.path.basename(file_path), f)} data = {'parent_folder': parent_folder} resp = self._request('POST', '/upload', files=files, data=data) if resp and resp.status_code == 201: return resp.json() return None def download_file(self, file_id, target_path): """下载文件""" resp = self._request('GET', f'/download/{file_id}') if resp and resp.status_code == 200: with open(target_path, 'wb') as f: f.write(resp.content) return True return False def delete_item(self, item_id): """删除文件或文件夹""" resp = self._request('DELETE', f'/delete/{item_id}') if resp: return resp.status_code == 200 return False def rename_item(self, item_id, new_name): """重命名文件或文件夹""" resp = self._request('PUT', f'/rename/{item_id}', json={'new_name': new_name}) if resp: return resp.status_code == 200 return False def move_item(self, item_id, target_folder): """移动文件或文件夹""" resp = self._request('POST', '/move', json={'item_id': item_id, 'target_folder': target_folder}) if resp: return resp.status_code == 200 return False def search_files(self, keyword): """搜索文件""" resp = self._request('GET', '/search', params={'q': keyword}) if resp and resp.status_code == 200: return resp.json() return None def get_storage_info(self): """获取存储信息""" resp = self._request('GET', '/storage_info') if resp and resp.status_code == 200: return resp.json() return None def get_user_info(self): """获取用户信息""" resp = self._request('GET', '/user_info') if resp and resp.status_code == 200: return resp.json() return None # ==================== 登录对话框 ==================== class LoginDialog(QDialog): """登录对话框""" def __init__(self, client, parent=None): super().__init__(parent) if DEBUG: debug_log("初始化登录对话框") self.client = client self.setWindowTitle("云笔记 - 登录") self.setFixedSize(400, 350) self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog) self.setAttribute(Qt.WA_TranslucentBackground) self.setup_ui() def setup_ui(self): """设置UI""" layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) card = QWidget() card.setObjectName("card") card.setStyleSheet(""" #card { background-color: white; border-radius: 16px; } QLineEdit { border: 1px solid #e1e4e8; border-radius: 10px; padding: 12px; font-size: 14px; } QPushButton { border-radius: 10px; padding: 12px; font-size: 14px; font-weight: 500; } """) card_layout = QVBoxLayout(card) card_layout.setContentsMargins(40, 40, 40, 40) card_layout.setSpacing(20) title = QLabel("☁️ 云笔记") title.setStyleSheet("font-size: 28px; font-weight: bold;") title.setAlignment(Qt.AlignCenter) card_layout.addWidget(title) self.email_input = QLineEdit() self.email_input.setPlaceholderText("电子邮箱") self.email_input.setMinimumHeight(45) card_layout.addWidget(self.email_input) self.password_input = QLineEdit() self.password_input.setPlaceholderText("密码") self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setMinimumHeight(45) card_layout.addWidget(self.password_input) self.remember_check = QCheckBox("记住我") card_layout.addWidget(self.remember_check) self.login_btn = QPushButton("登 录") self.login_btn.setMinimumHeight(45) self.login_btn.setStyleSheet("background-color: #0366d6; color: white; border: none;") self.login_btn.clicked.connect(self.do_login) card_layout.addWidget(self.login_btn) register_btn = QPushButton("还没有账号?立即注册") register_btn.setStyleSheet("background: transparent; color: #0366d6; border: none;") register_btn.clicked.connect(self.show_register) card_layout.addWidget(register_btn, alignment=Qt.AlignCenter) close_btn = QPushButton("×") close_btn.setFixedSize(30, 30) close_btn.setStyleSheet("background: transparent; border: none; font-size: 20px;") close_btn.clicked.connect(self.reject) layout.addWidget(card) close_btn.setParent(self) close_btn.move(self.width() - 40, 15) def do_login(self): """执行登录""" email = self.email_input.text().strip() password = self.password_input.text().strip() if not email or not password: QMessageBox.warning(self, "提示", "请填写邮箱和密码") return data = self.client.login(email, password) if data: if self.remember_check.isChecked(): settings = QSettings("CloudNote", "User") settings.setValue("token", self.client.token) settings.setValue("user_id", self.client.user_id) settings.setValue("username", self.client.username) self.accept() else: QMessageBox.warning(self, "错误", "邮箱或密码错误") def show_register(self): """显示注册对话框""" dialog = RegisterDialog(self.client, self) dialog.exec_() # ==================== 注册对话框 ==================== class RegisterDialog(QDialog): """注册对话框""" def __init__(self, client, parent=None): super().__init__(parent) if DEBUG: debug_log("初始化注册对话框") self.client = client self.setWindowTitle("注册账号") self.setFixedSize(400, 450) self.setWindowFlags(Qt.FramelessWindowHint | Qt.Dialog) self.setAttribute(Qt.WA_TranslucentBackground) self.setup_ui() def setup_ui(self): """设置UI""" layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) card = QWidget() card.setObjectName("card") card.setStyleSheet(""" #card { background-color: white; border-radius: 16px; } QLineEdit { border: 1px solid #e1e4e8; border-radius: 10px; padding: 12px; font-size: 14px; } QPushButton { border-radius: 10px; padding: 12px; font-size: 14px; font-weight: 500; } """) card_layout = QVBoxLayout(card) card_layout.setContentsMargins(40, 40, 40, 40) card_layout.setSpacing(15) title = QLabel("📝 创建新账号") title.setStyleSheet("font-size: 24px; font-weight: bold;") title.setAlignment(Qt.AlignCenter) card_layout.addWidget(title) self.username_input = QLineEdit() self.username_input.setPlaceholderText("用户名") self.username_input.setMinimumHeight(45) card_layout.addWidget(self.username_input) self.email_input = QLineEdit() self.email_input.setPlaceholderText("电子邮箱") self.email_input.setMinimumHeight(45) card_layout.addWidget(self.email_input) self.password_input = QLineEdit() self.password_input.setPlaceholderText("密码(至少6位)") self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setMinimumHeight(45) card_layout.addWidget(self.password_input) self.confirm_input = QLineEdit() self.confirm_input.setPlaceholderText("确认密码") self.confirm_input.setEchoMode(QLineEdit.Password) self.confirm_input.setMinimumHeight(45) card_layout.addWidget(self.confirm_input) self.register_btn = QPushButton("注 册") self.register_btn.setMinimumHeight(45) self.register_btn.setStyleSheet("background-color: #0366d6; color: white; border: none;") self.register_btn.clicked.connect(self.do_register) card_layout.addWidget(self.register_btn) close_btn = QPushButton("×") close_btn.setFixedSize(30, 30) close_btn.setStyleSheet("background: transparent; border: none; font-size: 20px;") close_btn.clicked.connect(self.reject) layout.addWidget(card) close_btn.setParent(self) close_btn.move(self.width() - 40, 15) def do_register(self): """执行注册""" username = self.username_input.text().strip() email = self.email_input.text().strip() password = self.password_input.text().strip() confirm = self.confirm_input.text().strip() if not all([username, email, password]): QMessageBox.warning(self, "提示", "请填写所有字段") return if password != confirm: QMessageBox.warning(self, "错误", "两次密码不一致") return if len(password) < 6: QMessageBox.warning(self, "错误", "密码至少6位") return res = self.client.register(username, email, password) if res: QMessageBox.information(self, "成功", "注册成功,请登录") self.accept() else: QMessageBox.warning(self, "错误", "注册失败,用户名或邮箱已存在") # ==================== 新建加密文件对话框 ==================== class NewEncryptedFileDialog(QDialog): """新建加密文件配置对话框""" def __init__(self, parent=None): super().__init__(parent) if DEBUG: debug_log("初始化新建加密文件对话框") self.setWindowTitle("新建加密笔记") self.setFixedSize(600, 700) self.selected_algorithm = 'rsa' self.selected_key_mode = 'global' self.use_password = False self.strict_password = True self.custom_password = None self.single_private = None self.single_public = None self.setup_ui() def setup_ui(self): """设置UI""" layout = QVBoxLayout(self) layout.setSpacing(15) title = QLabel("🔐 加密配置") title.setStyleSheet("font-size: 18px; font-weight: bold;") layout.addWidget(title) group1 = QGroupBox("1. 选择加密算法") group1_layout = QVBoxLayout() self.radio_rsa = QRadioButton("RSA-4096 + AES-256-GCM") self.radio_x25519 = QRadioButton("X25519 + AES-256-GCM") self.radio_rsa.setChecked(True) group1_layout.addWidget(self.radio_rsa) group1_layout.addWidget(self.radio_x25519) group1.setLayout(group1_layout) layout.addWidget(group1) group2 = QGroupBox("2. 选择密钥方式") group2_layout = QVBoxLayout() self.radio_global = QRadioButton("默认加密(使用全局密钥对)") self.radio_single = QRadioButton("安全加密(为当前文件单独生成密钥对)") self.radio_global.setChecked(True) group2_layout.addWidget(self.radio_global) group2_layout.addWidget(self.radio_single) group2.setLayout(group2_layout) layout.addWidget(group2) group3 = QGroupBox("3. 额外密码保护") group3_layout = QVBoxLayout() strength_layout = QHBoxLayout() strength_layout.addWidget(QLabel("密码强度要求:")) self.radio_strict = QRadioButton("高强度 (推荐)") self.radio_weak = QRadioButton("低强度 (不推荐)") self.radio_strict.setChecked(True) strength_layout.addWidget(self.radio_strict) strength_layout.addWidget(self.radio_weak) group3_layout.addLayout(strength_layout) self.checkbox_password = QCheckBox("启用双层加密") self.checkbox_password.toggled.connect(self.on_password_toggled) group3_layout.addWidget(self.checkbox_password) self.password_input = QLineEdit() self.password_input.setPlaceholderText("请输入密码(至少16位,包含大小写字母、数字和特殊字符)") self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setEnabled(False) group3_layout.addWidget(self.password_input) self.confirm_input = QLineEdit() self.confirm_input.setPlaceholderText("确认密码") self.confirm_input.setEchoMode(QLineEdit.Password) self.confirm_input.setEnabled(False) group3_layout.addWidget(self.confirm_input) self.password_strength_label = QLabel("") self.password_strength_label.setStyleSheet("font-size: 12px;") group3_layout.addWidget(self.password_strength_label) self.password_input.textChanged.connect(self.check_password_strength) self.radio_strict.toggled.connect(self.on_strength_mode_changed) self.radio_weak.toggled.connect(self.on_strength_mode_changed) group3.setLayout(group3_layout) layout.addWidget(group3) info_label = QLabel("说明:\n• 默认加密:使用全局密钥对,所有文件共用同一密钥\n• 安全加密:每个文件独立生成密钥对,私钥保存为 .key 文件\n• 密钥文件请妥善保管,丢失后将无法解密文件!\n• 高强度密码要求≥16位含大小写+数字+特殊字符\n• 低强度模式仅用于测试,不推荐用于重要数据") info_label.setStyleSheet("color: #586069; font-size: 12px; background-color: #f6f8fa; padding: 10px; border-radius: 8px;") info_label.setWordWrap(True) layout.addWidget(info_label) btn_layout = QHBoxLayout() self.ok_btn = QPushButton("确定") self.cancel_btn = QPushButton("取消") self.ok_btn.clicked.connect(self.accept_config) self.cancel_btn.clicked.connect(self.reject) btn_layout.addWidget(self.ok_btn) btn_layout.addWidget(self.cancel_btn) layout.addLayout(btn_layout) def on_strength_mode_changed(self): """密码强度模式改变""" self.strict_password = self.radio_strict.isChecked() if self.radio_strict.isChecked(): self.password_input.setPlaceholderText("请输入密码(至少16位,包含大小写字母、数字和特殊字符)") else: self.password_input.setPlaceholderText("请输入密码(无强度限制,但强烈建议使用强密码)") self.check_password_strength() def on_password_toggled(self, checked): """密码开关切换""" self.password_input.setEnabled(checked) self.confirm_input.setEnabled(checked) self.use_password = checked if checked: self.check_password_strength() else: self.password_strength_label.setText("") def check_password_strength(self): """检查密码强度""" if not self.use_password: self.password_strength_label.setText("") return pwd = self.password_input.text() if self.radio_strict.isChecked(): if len(pwd) >= 16 and any(c.isupper() for c in pwd) and any(c.islower() for c in pwd) and any(c.isdigit() for c in pwd) and any(not c.isalnum() for c in pwd): self.password_strength_label.setText("✓ 密码强度:强") self.password_strength_label.setStyleSheet("color: green; font-size: 12px;") elif len(pwd) >= 12: self.password_strength_label.setText("⚠ 密码强度:中(建议至少16位,包含大小写+数字+特殊字符)") self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;") else: self.password_strength_label.setText("✗ 密码强度:弱(需要至少16位,包含大小写+数字+特殊字符)") self.password_strength_label.setStyleSheet("color: red; font-size: 12px;") else: if len(pwd) >= 8: self.password_strength_label.setText("✓ 密码已输入(低强度模式,建议使用高强度)") self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;") elif len(pwd) > 0: self.password_strength_label.setText("⚠ 密码太短(建议至少8位)") self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;") else: self.password_strength_label.setText("请输入密码") self.password_strength_label.setStyleSheet("color: gray; font-size: 12px;") def accept_config(self): """接受配置""" if DEBUG: debug_log("开始获取加密配置") if self.radio_rsa.isChecked(): self.selected_algorithm = 'rsa' else: self.selected_algorithm = 'x25519' if DEBUG: debug_log(f"算法: {self.selected_algorithm}") if self.radio_global.isChecked(): self.selected_key_mode = 'global' else: self.selected_key_mode = 'single' if DEBUG: debug_log(f"密钥模式: {self.selected_key_mode}") self.strict_password = self.radio_strict.isChecked() if DEBUG: debug_log(f"密码强度模式: {'高强度' if self.strict_password else '低强度'}") if self.use_password: pwd = self.password_input.text() confirm = self.confirm_input.text() if not pwd or not confirm: QMessageBox.warning(self, "提示", "请输入密码") return if pwd != confirm: QMessageBox.warning(self, "错误", "两次输入的密码不一致") return if self.strict_password and not is_password_strong(pwd, strict=True): QMessageBox.warning(self, "密码强度不足", "密码必须至少16位,包含大小写字母、数字和特殊字符\n\n" "或切换到「低强度」模式(不推荐)") return self.custom_password = pwd if DEBUG: debug_log("密码验证通过") if self.selected_key_mode == 'single': if DEBUG: debug_log("生成单独密钥对") if self.selected_algorithm == 'rsa': self.single_private, self.single_public = generate_rsa_key(4096) else: self.single_private, self.single_public = generate_x25519_keys() if DEBUG: debug_log("加密配置获取完成") self.accept() def get_config(self): """获取配置""" return { 'algorithm': self.selected_algorithm, 'key_mode': self.selected_key_mode, 'use_password': self.use_password, 'strict_password': self.strict_password, 'custom_password': self.custom_password, 'single_private': self.single_private, 'single_public': self.single_public } # ==================== 保存加密文件对话框 ==================== class SaveEncryptedFileDialog(QDialog): """保存加密文件对话框""" def __init__(self, content, algorithm, key_mode, use_password, strict_password, custom_password, single_private, single_public, parent=None): super().__init__(parent) if DEBUG: debug_log("初始化保存加密文件对话框") self.content = content self.algorithm = algorithm self.key_mode = key_mode self.use_password = use_password self.strict_password = strict_password self.custom_password = custom_password self.single_private = single_private self.single_public = single_public self.setWindowTitle("保存加密笔记") self.setFixedSize(650, 400) self.selected_file_path = None self.setup_ui() def setup_ui(self): """设置UI""" layout = QVBoxLayout(self) layout.setSpacing(15) if self.key_mode == 'single': info_label = QLabel("⚠️ 您选择了【安全加密】模式,将为当前文件单独生成密钥对。\n密钥文件(.key)将与.bjb文件保存在同一目录,请妥善保管!\n丢失密钥文件后将无法解密!") info_label.setStyleSheet("color: #e36209; background-color: #fff5eb; padding: 10px; border-radius: 8px;") info_label.setWordWrap(True) layout.addWidget(info_label) layout.addWidget(QLabel("📁 保存位置和文件名:")) file_select_layout = QHBoxLayout() self.file_path_input = QLineEdit() self.file_path_input.setPlaceholderText("请选择保存位置...") self.file_path_input.setMinimumHeight(35) self.file_path_input.textChanged.connect(self.on_path_changed) file_select_layout.addWidget(self.file_path_input) self.browse_btn = QPushButton("浏览...") self.browse_btn.clicked.connect(self.select_save_file) file_select_layout.addWidget(self.browse_btn) layout.addLayout(file_select_layout) info_group = QGroupBox("加密信息摘要") info_layout = QVBoxLayout() algo_text = "RSA-4096" if self.algorithm == 'rsa' else "X25519" key_text = "全局密钥" if self.key_mode == 'global' else "单独密钥(安全模式)" pwd_text = "已启用" if self.use_password else "未启用" strength_text = "高强度" if self.strict_password else "低强度" info_layout.addWidget(QLabel(f"🔐 加密算法: {algo_text} + AES-256-GCM")) info_layout.addWidget(QLabel(f"🔑 密钥方式: {key_text}")) info_layout.addWidget(QLabel(f"🔒 密码保护: {pwd_text}")) if self.use_password: info_layout.addWidget(QLabel(f"🔐 密码强度: {strength_text}")) info_group.setLayout(info_layout) layout.addWidget(info_group) btn_layout = QHBoxLayout() self.ok_btn = QPushButton("💾 保存") self.ok_btn.setStyleSheet("background-color: #28a745; color: white;") self.cancel_btn = QPushButton("取消") self.ok_btn.clicked.connect(self.do_save) self.cancel_btn.clicked.connect(self.reject) btn_layout.addWidget(self.ok_btn) btn_layout.addWidget(self.cancel_btn) layout.addLayout(btn_layout) def on_path_changed(self, text): """路径改变""" if text.strip(): self.selected_file_path = text.strip() def select_save_file(self): """选择保存文件""" file_path, _ = QFileDialog.getSaveFileName(self, "保存加密笔记", "", "加密笔记 (*.bjb)") if file_path: if not file_path.endswith('.bjb'): file_path += '.bjb' self.selected_file_path = file_path self.file_path_input.setText(file_path) def do_save(self): """执行保存""" if not self.selected_file_path: QMessageBox.warning(self, "提示", "请选择保存位置") return if not self.selected_file_path.endswith('.bjb'): self.selected_file_path += '.bjb' try: encrypted_data, priv, pub = EncryptedNoteHandler.encrypt_content( self.content, self.algorithm, self.key_mode, self.custom_password, self.single_private, self.single_public, strict_password=self.strict_password ) except Exception as e: QMessageBox.critical(self, "加密失败", str(e)) return try: with open(self.selected_file_path, 'wb') as f: f.write(encrypted_data) except Exception as e: QMessageBox.critical(self, "保存失败", f"无法保存.bjb文件: {str(e)}") return key_path = None if self.key_mode == 'single' and priv: key_path = self.selected_file_path + '.key' try: with open(key_path, 'wb') as f: f.write(priv) except Exception as e: QMessageBox.critical(self, "保存失败", f"无法保存.key文件: {str(e)}") return msg = f"✅ 文件已保存!\n\n📄 .bjb文件:{self.selected_file_path}" if key_path: msg += f"\n🔑 .key文件:{key_path}\n\n⚠️ 请妥善保管密钥文件!" else: msg += "\n\n💡 提示:使用全局密钥,无需额外保存密钥文件。" QMessageBox.information(self, "保存成功", msg) self.accept() # ==================== 云端文件树窗口 ==================== class CloudFileTreeDialog(QDialog): """云端文件管理对话框""" def __init__(self, client, parent=None): super().__init__(parent) if DEBUG: debug_log("初始化云端文件树对话框") self.client = client self.setWindowTitle("云端文件管理") self.resize(900, 700) self.setup_ui() self.current_folder = '/' self.load_filetree() def setup_ui(self): """设置UI""" layout = QVBoxLayout(self) toolbar = QToolBar() self.up_btn = QAction("⬆ 返回上级", self) self.up_btn.triggered.connect(self.go_up) toolbar.addAction(self.up_btn) self.refresh_btn = QAction("🔄 刷新", self) self.refresh_btn.triggered.connect(self.load_filetree) toolbar.addAction(self.refresh_btn) self.new_folder_btn = QAction("📁 新建文件夹", self) self.new_folder_btn.triggered.connect(self.create_folder) toolbar.addAction(self.new_folder_btn) self.upload_btn = QAction("📤 上传文件", self) self.upload_btn.triggered.connect(self.upload_file) toolbar.addAction(self.upload_btn) layout.addWidget(toolbar) search_layout = QHBoxLayout() self.search_input = QLineEdit() self.search_input.setPlaceholderText("搜索文件...") self.search_input.returnPressed.connect(self.search_files) search_btn = QPushButton("搜索") search_btn.clicked.connect(self.search_files) clear_btn = QPushButton("清除") clear_btn.clicked.connect(self.clear_search) search_layout.addWidget(self.search_input) search_layout.addWidget(search_btn) search_layout.addWidget(clear_btn) layout.addLayout(search_layout) self.tree = QTreeWidget() self.tree.setHeaderLabels(["名称", "大小", "类型", "修改时间"]) self.tree.setAlternatingRowColors(True) self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect(self.show_context_menu) self.tree.itemDoubleClicked.connect(self.on_item_double_click) layout.addWidget(self.tree) self.status_label = QLabel("") layout.addWidget(self.status_label) self.load_storage_info() def load_storage_info(self): """加载存储信息""" info = self.client.get_storage_info() if info: used_mb = info['total_size'] / (1024 * 1024) max_mb = info['max_size'] / (1024 * 1024) download_used_mb = info['download_used'] / (1024 * 1024) download_max_mb = info['download_max'] / (1024 * 1024) self.status_label.setText(f"存储: {used_mb:.1f}MB / {max_mb:.0f}MB | 今日下载: {download_used_mb:.1f}MB / {download_max_mb:.0f}MB") def load_filetree(self): """加载文件树""" if DEBUG: debug_log(f"加载文件树: {self.current_folder}") data = self.client.get_filetree(self.current_folder) if not data: QMessageBox.warning(self, "错误", "加载文件树失败") return self.tree.clear() for item in data['items']: size_str = self.format_size(item['size']) if item['size'] else "" type_str = "文件夹" if item['is_folder'] else (item['type'] or "文件") tree_item = QTreeWidgetItem([item['name'], size_str, type_str, item['created_at']]) tree_item.setData(0, Qt.UserRole, item) self.tree.addTopLevelItem(tree_item) self.load_storage_info() def format_size(self, size_bytes): """格式化文件大小""" if not size_bytes: return "" for unit in ['B', 'KB', 'MB', 'GB']: if size_bytes < 1024.0: return f"{size_bytes:.1f}{unit}" size_bytes /= 1024.0 return f"{size_bytes:.1f}TB" def go_up(self): """返回上级目录""" if self.current_folder != '/': parts = self.current_folder.rstrip('/').split('/') if len(parts) > 1: self.current_folder = '/' + '/'.join(parts[:-1]) else: self.current_folder = '/' self.load_filetree() def create_folder(self): """创建文件夹""" name, ok = QInputDialog.getText(self, "新建文件夹", "请输入文件夹名称:") if ok and name: if '/' in name or '\\' in name: QMessageBox.warning(self, "错误", "文件夹名称不能包含路径分隔符") return res = self.client.create_folder(name, self.current_folder) if res: self.load_filetree() else: QMessageBox.warning(self, "错误", "创建失败") def upload_file(self): """上传文件""" file_path, _ = QFileDialog.getOpenFileName(self, "选择要上传的文件", "", "笔记本文件 (*.bjb *.txt)") if file_path: ext = os.path.splitext(file_path)[1].lower() if ext not in ['.txt', '.bjb']: QMessageBox.warning(self, "错误", "只支持上传 .txt 和 .bjb 文件") return file_size = os.path.getsize(file_path) info = self.client.get_storage_info() if info and info['total_size'] + file_size > info['max_size']: QMessageBox.warning(self, "错误", "云端存储空间不足") return res = self.client.upload_file(file_path, self.current_folder) if res: QMessageBox.information(self, "成功", "上传成功") self.load_filetree() else: QMessageBox.warning(self, "错误", "上传失败") def search_files(self): """搜索文件""" keyword = self.search_input.text().strip() if not keyword: return data = self.client.search_files(keyword) if data: self.tree.clear() for item in data['items']: size_str = self.format_size(item['size']) if item['size'] else "" type_str = "文件夹" if item['is_folder'] else (item['type'] or "文件") tree_item = QTreeWidgetItem([item['name'], size_str, type_str, ""]) tree_item.setData(0, Qt.UserRole, item) self.tree.addTopLevelItem(tree_item) self.status_label.setText(f"搜索到 {len(data['items'])} 个结果") def clear_search(self): """清除搜索""" self.search_input.clear() self.load_filetree() def on_item_double_click(self, item, col): """双击项目""" data = item.data(0, Qt.UserRole) if data['is_folder']: if self.search_input.text(): self.clear_search() self.current_folder = data['parent'] + '/' + data['name'] if data['parent'] != '/' else '/' + data['name'] self.load_filetree() else: self.download_and_open(data) def download_and_open(self, file_info): """ 下载并打开云端文件 修复:支持单独密钥模式,弹出文件选择对话框让用户选择.key文件 """ temp_dir = tempfile.gettempdir() local_path = os.path.join(temp_dir, f"cloud_{file_info['id']}_{file_info['name']}") if DEBUG: debug_log(f"下载文件到: {local_path}") debug_log(f"文件信息: ID={file_info['id']}, Name={file_info['name']}, Type={file_info.get('type', 'unknown')}") if self.client.download_file(file_info['id'], local_path): if DEBUG: debug_log("下载成功") ext = os.path.splitext(file_info['name'])[1].lower() if ext == '.txt': # 文本文件直接打开 try: with open(local_path, 'r', encoding='utf-8') as f: content = f.read() main_window = self.parent() if isinstance(main_window, MainWindow): tab = EditorTab(main_window.tabs, main_window.tabs.count(), file_path=local_path, cloud_id=file_info['id'], file_type='txt', content=content) main_window.tabs.addTab(tab.text_edit, os.path.basename(file_info['name'])) main_window.editor_tabs.append(tab) main_window.tabs.setCurrentIndex(main_window.tabs.count() - 1) self.accept() except Exception as e: debug_error(f"读取文本文件失败: {e}") QMessageBox.warning(self, "错误", f"读取文件失败: {str(e)}") else: # 加密文件:使用修复后的解密函数,传入云端文件标志和文件名 if DEBUG: debug_log("调用解密函数(云端文件模式)") # 先获取文件元数据,看看是否需要单独密钥 metadata = EncryptedNoteHandler.get_file_metadata(local_path) if metadata and metadata.get('single_key', False): if DEBUG: debug_log("检测到单独密钥模式,准备弹出密钥文件选择对话框") content, success = decrypt_bjb_file( local_path, self, is_cloud_file=True, cloud_filename=file_info['name'] ) if success and content is not None: if DEBUG: debug_log("解密成功,创建编辑器选项卡") main_window = self.parent() if isinstance(main_window, MainWindow): tab = EditorTab(main_window.tabs, main_window.tabs.count(), file_path=local_path, cloud_id=file_info['id'], file_type='bjb', content=content) main_window.tabs.addTab(tab.text_edit, os.path.basename(file_info['name'])) main_window.editor_tabs.append(tab) main_window.tabs.setCurrentIndex(main_window.tabs.count() - 1) self.accept() else: if DEBUG: debug_log(f"警告: parent 不是 MainWindow 类型: {type(main_window)}") else: if DEBUG: debug_log("解密失败,不关闭对话框") # 解密失败时不关闭对话框,让用户重试 else: if DEBUG: debug_log("下载失败") QMessageBox.warning(self, "错误", "下载失败") def show_context_menu(self, pos): """显示右键菜单""" item = self.tree.itemAt(pos) if not item: return data = item.data(0, Qt.UserRole) menu = QMenu() rename_action = menu.addAction("重命名") delete_action = menu.addAction("删除") move_action = menu.addAction("移动") download_action = menu.addAction("下载") action = menu.exec_(self.tree.viewport().mapToGlobal(pos)) if action == rename_action: new_name, ok = QInputDialog.getText(self, "重命名", "新名称:", text=data['name']) if ok and new_name: if self.client.rename_item(data['id'], new_name): self.load_filetree() else: QMessageBox.warning(self, "错误", "重命名失败") elif action == delete_action: if QMessageBox.question(self, "确认", f"确定删除 {data['name']} 吗?") == QMessageBox.Yes: if self.client.delete_item(data['id']): self.load_filetree() else: QMessageBox.warning(self, "错误", "删除失败") elif action == move_action: target, ok = QInputDialog.getText(self, "移动", "目标文件夹路径 (例如 /folder):") if ok and target: if self.client.move_item(data['id'], target): self.load_filetree() else: QMessageBox.warning(self, "错误", "移动失败") elif action == download_action: save_path, _ = QFileDialog.getSaveFileName(self, "保存文件", data['name']) if save_path: if self.client.download_file(data['id'], save_path): QMessageBox.information(self, "成功", "下载完成") else: QMessageBox.warning(self, "错误", "下载失败") # ==================== 编辑器选项卡 ==================== class EditorTab: """编辑器选项卡类""" def __init__(self, tab_widget, index, file_path=None, cloud_id=None, is_new=False, content="", file_type="txt", encrypt_config=None): if DEBUG: debug_log(f"创建编辑器选项卡 - 类型: {file_type}, 新文件: {is_new}, 路径: {file_path}") self.tab_widget = tab_widget self.index = index self.file_path = file_path self.cloud_id = cloud_id self.is_new = is_new self.file_type = file_type self.encrypt_config = encrypt_config self.modified = False self.text_edit = QTextEdit() self.text_edit.setPlainText(content) self.text_edit.textChanged.connect(self.on_text_changed) font = QFont("Consolas", 12) self.text_edit.setFont(font) self.update_tab_title() def update_tab_title(self): """更新选项卡标题""" if self.file_path: name = os.path.basename(self.file_path) elif self.cloud_id is not None: name = f"云端文件_{self.cloud_id}" else: name = "未命名" if self.file_type == 'bjb': name = f"🔒 {name}" if self.modified: name = "* " + name self.tab_widget.setTabText(self.index, name) def on_text_changed(self): """文本改变时的回调""" if not self.modified: self.modified = True self.update_tab_title() def get_content(self): """获取内容""" return self.text_edit.toPlainText() def set_content(self, content): """设置内容""" self.text_edit.setPlainText(content) self.modified = False self.update_tab_title() def mark_saved(self): """标记已保存""" self.modified = False self.update_tab_title() # ==================== 加密当前文件对话框 ==================== class EncryptCurrentFileDialog(QDialog): """加密当前文件对话框""" def __init__(self, parent=None): super().__init__(parent) if DEBUG: debug_log("初始化加密当前文件对话框") self.setWindowTitle("加密当前文件") self.setFixedSize(600, 600) self.selected_algorithm = 'rsa' self.selected_key_mode = 'global' self.use_password = False self.strict_password = True self.custom_password = None self.single_private = None self.single_public = None self.setup_ui() def setup_ui(self): """设置UI""" layout = QVBoxLayout(self) layout.setSpacing(15) title = QLabel("🔐 加密当前文件配置") title.setStyleSheet("font-size: 18px; font-weight: bold;") layout.addWidget(title) info_label = QLabel("⚠️ 注意:加密后将生成新的.bjb文件,原文件不会被删除") info_label.setStyleSheet("color: #e36209; background-color: #fff5eb; padding: 8px; border-radius: 8px;") layout.addWidget(info_label) group1 = QGroupBox("1. 选择加密算法") group1_layout = QVBoxLayout() self.radio_rsa = QRadioButton("RSA-4096 + AES-256-GCM") self.radio_x25519 = QRadioButton("X25519 + AES-256-GCM") self.radio_rsa.setChecked(True) group1_layout.addWidget(self.radio_rsa) group1_layout.addWidget(self.radio_x25519) group1.setLayout(group1_layout) layout.addWidget(group1) group2 = QGroupBox("2. 选择密钥方式") group2_layout = QVBoxLayout() self.radio_global = QRadioButton("默认加密(使用全局密钥对)") self.radio_single = QRadioButton("安全加密(为当前文件单独生成密钥对)") self.radio_global.setChecked(True) group2_layout.addWidget(self.radio_global) group2_layout.addWidget(self.radio_single) group2.setLayout(group2_layout) layout.addWidget(group2) group3 = QGroupBox("3. 额外密码保护") group3_layout = QVBoxLayout() strength_layout = QHBoxLayout() strength_layout.addWidget(QLabel("密码强度要求:")) self.radio_strict = QRadioButton("高强度 (推荐)") self.radio_weak = QRadioButton("低强度 (不推荐)") self.radio_strict.setChecked(True) strength_layout.addWidget(self.radio_strict) strength_layout.addWidget(self.radio_weak) group3_layout.addLayout(strength_layout) self.checkbox_password = QCheckBox("启用双层加密") self.checkbox_password.toggled.connect(self.on_password_toggled) group3_layout.addWidget(self.checkbox_password) self.password_input = QLineEdit() self.password_input.setPlaceholderText("请输入密码(至少16位,包含大小写字母、数字和特殊字符)") self.password_input.setEchoMode(QLineEdit.Password) self.password_input.setEnabled(False) group3_layout.addWidget(self.password_input) self.confirm_input = QLineEdit() self.confirm_input.setPlaceholderText("确认密码") self.confirm_input.setEchoMode(QLineEdit.Password) self.confirm_input.setEnabled(False) group3_layout.addWidget(self.confirm_input) self.password_strength_label = QLabel("") self.password_strength_label.setStyleSheet("font-size: 12px;") group3_layout.addWidget(self.password_strength_label) self.password_input.textChanged.connect(self.check_password_strength) self.radio_strict.toggled.connect(self.on_strength_mode_changed) group3.setLayout(group3_layout) layout.addWidget(group3) btn_layout = QHBoxLayout() self.ok_btn = QPushButton("加密并保存") self.ok_btn.setStyleSheet("background-color: #28a745; color: white;") self.cancel_btn = QPushButton("取消") self.ok_btn.clicked.connect(self.accept_config) self.cancel_btn.clicked.connect(self.reject) btn_layout.addWidget(self.ok_btn) btn_layout.addWidget(self.cancel_btn) layout.addLayout(btn_layout) def on_strength_mode_changed(self): """密码强度模式改变""" self.strict_password = self.radio_strict.isChecked() if self.radio_strict.isChecked(): self.password_input.setPlaceholderText("请输入密码(至少16位,包含大小写字母、数字和特殊字符)") else: self.password_input.setPlaceholderText("请输入密码(无强度限制,但强烈建议使用强密码)") self.check_password_strength() def on_password_toggled(self, checked): """密码开关切换""" self.password_input.setEnabled(checked) self.confirm_input.setEnabled(checked) self.use_password = checked if checked: self.check_password_strength() else: self.password_strength_label.setText("") def check_password_strength(self): """检查密码强度""" if not self.use_password: self.password_strength_label.setText("") return pwd = self.password_input.text() if self.radio_strict.isChecked(): if len(pwd) >= 16 and any(c.isupper() for c in pwd) and any(c.islower() for c in pwd) and any(c.isdigit() for c in pwd) and any(not c.isalnum() for c in pwd): self.password_strength_label.setText("✓ 密码强度:强") self.password_strength_label.setStyleSheet("color: green; font-size: 12px;") elif len(pwd) >= 12: self.password_strength_label.setText("⚠ 密码强度:中(建议至少16位)") self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;") else: self.password_strength_label.setText("✗ 密码强度:弱(需要至少16位)") self.password_strength_label.setStyleSheet("color: red; font-size: 12px;") else: if len(pwd) >= 8: self.password_strength_label.setText("✓ 密码已输入(低强度模式)") self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;") elif len(pwd) > 0: self.password_strength_label.setText("⚠ 密码太短") self.password_strength_label.setStyleSheet("color: orange; font-size: 12px;") else: self.password_strength_label.setText("请输入密码") self.password_strength_label.setStyleSheet("color: gray; font-size: 12px;") def accept_config(self): """接受配置""" if DEBUG: debug_log("开始获取加密配置") if self.radio_rsa.isChecked(): self.selected_algorithm = 'rsa' else: self.selected_algorithm = 'x25519' if DEBUG: debug_log(f"算法: {self.selected_algorithm}") if self.radio_global.isChecked(): self.selected_key_mode = 'global' else: self.selected_key_mode = 'single' if DEBUG: debug_log(f"密钥模式: {self.selected_key_mode}") self.strict_password = self.radio_strict.isChecked() if DEBUG: debug_log(f"密码强度模式: {'高强度' if self.strict_password else '低强度'}") if self.use_password: pwd = self.password_input.text() confirm = self.confirm_input.text() if not pwd or not confirm: QMessageBox.warning(self, "提示", "请输入密码") return if pwd != confirm: QMessageBox.warning(self, "错误", "两次输入的密码不一致") return if self.strict_password and not is_password_strong(pwd, strict=True): QMessageBox.warning(self, "密码强度不足", "密码必须至少16位,包含大小写字母、数字和特殊字符\n\n" "或切换到「低强度」模式(不推荐)") return self.custom_password = pwd if DEBUG: debug_log("密码验证通过") if self.selected_key_mode == 'single': if DEBUG: debug_log("生成单独密钥对") if self.selected_algorithm == 'rsa': self.single_private, self.single_public = generate_rsa_key(4096) else: self.single_private, self.single_public = generate_x25519_keys() if DEBUG: debug_log("加密配置获取完成") self.accept() def get_config(self): """获取配置""" return { 'algorithm': self.selected_algorithm, 'key_mode': self.selected_key_mode, 'use_password': self.use_password, 'strict_password': self.strict_password, 'custom_password': self.custom_password, 'single_private': self.single_private, 'single_public': self.single_public } # ==================== 主窗口 ==================== class MainWindow(QMainWindow): """主窗口类""" def __init__(self): super().__init__() if DEBUG: debug_log("初始化主窗口") self.client = CloudClient() self.tabs = QTabWidget() self.tabs.setTabsClosable(True) self.tabs.tabCloseRequested.connect(self.close_tab) self.tabs.currentChanged.connect(self.on_tab_changed) self.setCentralWidget(self.tabs) self.setWindowTitle(f"云笔记 - 安全加密笔记") self.resize(1200, 800) self.setup_menu() self.setup_statusbar() self.editor_tabs = [] self.pending_encrypt_config = None self.current_editor = None ensure_global_keys() self.check_login() def setup_menu(self): """设置菜单栏""" menubar = self.menuBar() file_menu = menubar.addMenu("文件") new_txt_action = QAction("新建文本文件(.txt)", self) new_txt_action.setShortcut(QKeySequence("Ctrl+N")) new_txt_action.triggered.connect(lambda: self.new_file('txt')) file_menu.addAction(new_txt_action) new_bjb_action = QAction("新建加密笔记(.bjb)...", self) new_bjb_action.setShortcut(QKeySequence("Ctrl+Shift+N")) new_bjb_action.triggered.connect(lambda: self.new_file('bjb')) file_menu.addAction(new_bjb_action) file_menu.addSeparator() open_action = QAction("打开文件...", self) open_action.setShortcut(QKeySequence.Open) open_action.triggered.connect(self.open_local_file_dialog) file_menu.addAction(open_action) open_cloud_action = QAction("从云端打开...", self) open_cloud_action.setShortcut(QKeySequence("Ctrl+Shift+O")) open_cloud_action.triggered.connect(self.open_cloud_file_dialog) file_menu.addAction(open_cloud_action) file_menu.addSeparator() save_action = QAction("保存", self) save_action.setShortcut(QKeySequence.Save) save_action.triggered.connect(self.save_current_tab) file_menu.addAction(save_action) save_as_action = QAction("另存为...", self) save_as_action.setShortcut(QKeySequence.SaveAs) save_as_action.triggered.connect(self.save_as_current_tab) file_menu.addAction(save_as_action) file_menu.addSeparator() exit_action = QAction("退出", self) exit_action.setShortcut(QKeySequence.Quit) exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # 加密菜单 encrypt_menu = menubar.addMenu("加密") encrypt_current_action = QAction("🔐 加密当前文件...", self) encrypt_current_action.setShortcut(QKeySequence("Ctrl+E")) encrypt_current_action.triggered.connect(self.encrypt_current_file) encrypt_menu.addAction(encrypt_current_action) encrypt_menu.addSeparator() txt_to_bjb_action = QAction("📄 另存为加密笔记(.bjb)...", self) txt_to_bjb_action.setShortcut(QKeySequence("Ctrl+Shift+S")) txt_to_bjb_action.triggered.connect(self.convert_txt_to_bjb) encrypt_menu.addAction(txt_to_bjb_action) cloud_menu = menubar.addMenu("云端") manage_cloud_action = QAction("管理云端文件", self) manage_cloud_action.triggered.connect(self.show_cloud_manager) cloud_menu.addAction(manage_cloud_action) upload_action = QAction("上传当前文件到云端", self) upload_action.triggered.connect(self.upload_current_tab) cloud_menu.addAction(upload_action) cloud_menu.addSeparator() logout_action = QAction("退出登录", self) logout_action.triggered.connect(self.logout) cloud_menu.addAction(logout_action) help_menu = menubar.addMenu("帮助") about_action = QAction("关于", self) about_action.triggered.connect(self.about) help_menu.addAction(about_action) def setup_statusbar(self): """设置状态栏""" self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) self.storage_label = QLabel("") self.status_bar.addPermanentWidget(self.storage_label) self.update_storage_info() def update_storage_info(self): """更新存储信息""" if self.client.token: info = self.client.get_storage_info() if info: used_mb = info['total_size'] / (1024 * 1024) max_mb = info['max_size'] / (1024 * 1024) self.storage_label.setText(f"📁 {used_mb:.1f}MB / {max_mb:.0f}MB") def check_login(self): """检查登录状态""" settings = QSettings("CloudNote", "User") token = settings.value("token") user_id = settings.value("user_id") username = settings.value("username") if token and user_id and username: self.client.set_auth(token, int(user_id), username) user_info = self.client.get_user_info() if user_info: self.status_bar.showMessage(f"欢迎回来,{username}") self.update_storage_info() return else: settings.clear() self.client.clear_auth() dialog = LoginDialog(self.client, self) if dialog.exec_() == QDialog.Accepted: self.status_bar.showMessage(f"欢迎 {self.client.username}") self.update_storage_info() else: sys.exit(0) def logout(self): """退出登录""" self.client.clear_auth() settings = QSettings("CloudNote", "User") settings.clear() self.status_bar.showMessage("已退出登录") while self.tabs.count() > 0: self.close_tab(0) self.check_login() def new_file(self, file_type): """新建文件""" if DEBUG: debug_log(f"新建文件: {file_type}") if file_type == 'txt': tab = EditorTab(self.tabs, self.tabs.count(), file_type='txt', is_new=True, content="") self.tabs.addTab(tab.text_edit, "未命名") self.editor_tabs.append(tab) self.tabs.setCurrentIndex(self.tabs.count() - 1) else: # 先创建选项卡,再显示配置对话框 tab = EditorTab(self.tabs, self.tabs.count(), file_type='bjb', is_new=True, content="") self.tabs.addTab(tab.text_edit, "未命名加密笔记") self.editor_tabs.append(tab) self.tabs.setCurrentIndex(self.tabs.count() - 1) dialog = NewEncryptedFileDialog(self) if dialog.exec_() == QDialog.Accepted: config = dialog.get_config() tab.encrypt_config = config self.save_as_current_tab() else: self.close_tab(self.tabs.currentIndex()) def open_local_file_dialog(self): """打开本地文件对话框""" if DEBUG: debug_log("打开本地文件对话框") file_path, _ = QFileDialog.getOpenFileName( self, "打开文件", "", "笔记本文件 (*.bjb *.txt)" ) if file_path: if DEBUG: debug_log(f"用户选择文件: {file_path}") self.open_local_file(file_path) def open_local_file(self, file_path, cloud_id=None): """ 打开本地文件 修复:使用修复后的解密函数,支持单独密钥模式 """ ext = os.path.splitext(file_path)[1].lower() if DEBUG: debug_log(f"打开本地文件: {file_path}, 类型: {ext}") if ext == '.txt': try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() tab = EditorTab(self.tabs, self.tabs.count(), file_path=file_path, cloud_id=cloud_id, file_type='txt', content=content) self.tabs.addTab(tab.text_edit, os.path.basename(file_path)) self.editor_tabs.append(tab) self.tabs.setCurrentIndex(self.tabs.count() - 1) except Exception as e: debug_error(f"读取文件失败: {e}") QMessageBox.critical(self, "错误", f"读取文件失败: {str(e)}") elif ext == '.bjb': # 本地文件,is_cloud_file=False,会优先查找同目录.key文件 if DEBUG: debug_log("调用解密函数(本地文件模式)") content, success = decrypt_bjb_file(file_path, self, is_cloud_file=False) if success and content is not None: tab = EditorTab(self.tabs, self.tabs.count(), file_path=file_path, cloud_id=cloud_id, file_type='bjb', content=content) self.tabs.addTab(tab.text_edit, os.path.basename(file_path)) self.editor_tabs.append(tab) self.tabs.setCurrentIndex(self.tabs.count() - 1) else: if DEBUG: debug_log("解密失败,不创建选项卡") def open_cloud_file_dialog(self): """打开云端文件对话框""" if not self.client.token: QMessageBox.warning(self, "提示", "请先登录") self.check_login() return dialog = CloudFileTreeDialog(self.client, self) dialog.exec_() def save_current_tab(self): """保存当前选项卡 - 加密文件直接使用原有配置静默保存""" idx = self.tabs.currentIndex() if idx < 0 or idx >= len(self.editor_tabs): if DEBUG: debug_log(f"保存失败: 无效索引 {idx}") return tab = self.editor_tabs[idx] content = tab.get_content() if DEBUG: debug_log(f"保存当前选项卡: 索引 {idx}, 类型: {tab.file_type}, 新文件: {tab.is_new}") debug_log(f"内容长度: {len(content)}字符") # 新文件:调用另存为(会弹出配置对话框) if tab.is_new: if DEBUG: debug_log("新文件,调用另存为") self.save_as_current_tab() return # 已有文件路径 if tab.file_path and os.path.exists(tab.file_path): try: if tab.file_type == 'txt': # 文本文件:直接写入 if DEBUG: debug_log(f"保存文本文件: {tab.file_path}") with open(tab.file_path, 'w', encoding='utf-8') as f: f.write(content) tab.mark_saved() self.status_bar.showMessage(f"已保存: {os.path.basename(tab.file_path)}", 3000) if DEBUG: debug_log("文本文件保存成功") else: # bjb 加密文件 if DEBUG: debug_log(f"保存加密文件: {tab.file_path}") # 检查是否有加密配置 if hasattr(tab, 'encrypt_config') and tab.encrypt_config: config = tab.encrypt_config if DEBUG: debug_log(f"使用现有加密配置 - 算法: {config.get('algorithm')}, 密钥模式: {config.get('key_mode')}, 密码保护: {config.get('use_password', False)}") # 使用原有配置加密(静默保存,不弹窗) encrypted_data, _, _ = EncryptedNoteHandler.encrypt_content( content, config.get('algorithm', 'rsa'), config.get('key_mode', 'global'), config.get('custom_password'), config.get('single_private'), config.get('single_public'), strict_password=config.get('strict_password', True) ) with open(tab.file_path, 'wb') as f: f.write(encrypted_data) tab.mark_saved() self.status_bar.showMessage(f"已保存: {os.path.basename(tab.file_path)}", 3000) if DEBUG: debug_log("加密文件保存成功(使用现有配置,静默保存)") else: # 没有加密配置(理论上不应该发生,但作为后备) if DEBUG: debug_log("加密配置缺失,弹出配置对话框") dialog = NewEncryptedFileDialog(self) if dialog.exec_() == QDialog.Accepted: config = dialog.get_config() tab.encrypt_config = config encrypted_data, _, _ = EncryptedNoteHandler.encrypt_content( content, config['algorithm'], config['key_mode'], config.get('custom_password'), config.get('single_private'), config.get('single_public'), strict_password=config.get('strict_password', True) ) with open(tab.file_path, 'wb') as f: f.write(encrypted_data) tab.mark_saved() self.status_bar.showMessage(f"已保存: {os.path.basename(tab.file_path)}", 3000) if DEBUG: debug_log("加密文件保存成功(新配置)") else: if DEBUG: debug_log("用户取消保存") return except Exception as e: debug_error(f"保存失败: {e}") QMessageBox.critical(self, "保存失败", str(e)) else: # 文件路径无效,调用另存为 if DEBUG: debug_log("文件路径无效,调用另存为") self.save_as_current_tab() def save_as_current_tab(self): """另存为当前选项卡 - 首次保存时弹出配置对话框""" idx = self.tabs.currentIndex() if idx < 0 or idx >= len(self.editor_tabs): if DEBUG: debug_log(f"另存为失败: 无效索引 {idx}") return tab = self.editor_tabs[idx] content = tab.get_content() if DEBUG: debug_log(f"另存为当前选项卡: 索引 {idx}, 类型: {tab.file_type}") debug_log(f"内容长度: {len(content)}字符") if tab.file_type == 'txt': # 文本文件另存为 if DEBUG: debug_log("另存为文本文件") file_path, _ = QFileDialog.getSaveFileName(self, "保存文件", "", "文本文件 (*.txt)") if not file_path: if DEBUG: debug_log("用户取消保存") return if not file_path.endswith('.txt'): file_path += '.txt' try: with open(file_path, 'w', encoding='utf-8') as f: f.write(content) tab.file_path = file_path tab.is_new = False tab.mark_saved() if DEBUG: debug_log(f"文本文件另存为成功: {file_path}") self.status_bar.showMessage(f"已保存: {os.path.basename(file_path)}", 3000) except Exception as e: debug_error(f"保存失败: {e}") QMessageBox.critical(self, "保存失败", str(e)) else: # bjb 加密文件 if DEBUG: debug_log("另存为加密文件") # 获取加密配置 if hasattr(tab, 'encrypt_config') and tab.encrypt_config: # 已有配置,直接使用(但另存为时应该让用户选择新位置) config = tab.encrypt_config if DEBUG: debug_log("使用选项卡中的现有加密配置") else: # 没有配置,弹出配置对话框 if DEBUG: debug_log("显示加密配置对话框") dialog = NewEncryptedFileDialog(self) if dialog.exec_() != QDialog.Accepted: if DEBUG: debug_log("用户取消加密配置") return config = dialog.get_config() tab.encrypt_config = config if DEBUG: debug_log("加密配置获取完成") # 弹出保存文件对话框 save_dialog = SaveEncryptedFileDialog( content, config['algorithm'], config['key_mode'], config['use_password'], config.get('strict_password', True), config.get('custom_password'), config.get('single_private'), config.get('single_public'), self ) if save_dialog.exec_() == QDialog.Accepted: tab.file_type = 'bjb' tab.encrypt_config = config tab.is_new = False if save_dialog.selected_file_path: tab.file_path = save_dialog.selected_file_path tab.mark_saved() if DEBUG: debug_log(f"加密文件另存为成功: {tab.file_path}") def encrypt_current_file(self): """加密当前文件""" if DEBUG: debug_log("加密当前文件功能被调用") idx = self.tabs.currentIndex() if idx < 0 or idx >= len(self.editor_tabs): QMessageBox.warning(self, "提示", "没有打开的文件") return tab = self.editor_tabs[idx] content = tab.get_content() if not content.strip(): ret = QMessageBox.question(self, "确认", "文件内容为空,是否继续加密?", QMessageBox.Yes | QMessageBox.No) if ret != QMessageBox.Yes: return dialog = EncryptCurrentFileDialog(self) if dialog.exec_() != QDialog.Accepted: return config = dialog.get_config() default_name = "encrypted_note.bjb" if tab.file_path: base_name = os.path.splitext(os.path.basename(tab.file_path))[0] default_name = f"{base_name}_encrypted.bjb" file_path, _ = QFileDialog.getSaveFileName(self, "保存加密文件", default_name, "加密笔记 (*.bjb)") if not file_path: return if not file_path.endswith('.bjb'): file_path += '.bjb' try: encrypted_data, priv, pub = EncryptedNoteHandler.encrypt_content( content, config['algorithm'], config['key_mode'], config['custom_password'], config.get('single_private'), config.get('single_public'), strict_password=config.get('strict_password', True) ) with open(file_path, 'wb') as f: f.write(encrypted_data) if config['key_mode'] == 'single' and priv: key_path = file_path + '.key' with open(key_path, 'wb') as f: f.write(priv) QMessageBox.information(self, "加密成功", f"文件已加密保存为:\n{file_path}\n\n" f"密钥文件已保存为:\n{key_path}\n\n" f"请妥善保管密钥文件!") else: QMessageBox.information(self, "加密成功", f"文件已加密保存为:\n{file_path}\n\n" f"使用全局密钥加密,无需额外保存密钥文件。") ret = QMessageBox.question(self, "打开文件", "是否在新选项卡中打开加密后的文件?", QMessageBox.Yes | QMessageBox.No) if ret == QMessageBox.Yes: self.open_local_file(file_path) except Exception as e: debug_error(f"加密失败: {e}") QMessageBox.critical(self, "加密失败", str(e)) def convert_txt_to_bjb(self): """将当前文本文件转换为加密笔记""" if DEBUG: debug_log("TXT转BJB功能被调用") idx = self.tabs.currentIndex() if idx < 0 or idx >= len(self.editor_tabs): QMessageBox.warning(self, "提示", "没有打开的文件") return tab = self.editor_tabs[idx] if tab.file_type == 'bjb': QMessageBox.information(self, "提示", "当前文件已经是加密笔记格式") return content = tab.get_content() dialog = EncryptCurrentFileDialog(self) if dialog.exec_() != QDialog.Accepted: return config = dialog.get_config() default_name = os.path.splitext(os.path.basename(tab.file_path))[0] + ".bjb" if tab.file_path else "converted.bjb" file_path, _ = QFileDialog.getSaveFileName(self, "保存为加密笔记", default_name, "加密笔记 (*.bjb)") if not file_path: return if not file_path.endswith('.bjb'): file_path += '.bjb' try: encrypted_data, priv, pub = EncryptedNoteHandler.encrypt_content( content, config['algorithm'], config['key_mode'], config['custom_password'], config.get('single_private'), config.get('single_public'), strict_password=config.get('strict_password', True) ) with open(file_path, 'wb') as f: f.write(encrypted_data) if config['key_mode'] == 'single' and priv: key_path = file_path + '.key' with open(key_path, 'wb') as f: f.write(priv) QMessageBox.information(self, "转换成功", f"文件已转换为加密笔记:\n{file_path}\n\n" f"密钥文件已保存为:\n{key_path}\n\n" f"请妥善保管密钥文件!") else: QMessageBox.information(self, "转换成功", f"文件已转换为加密笔记:\n{file_path}\n\n" f"使用全局密钥加密,无需额外保存密钥文件。") ret = QMessageBox.question(self, "打开文件", "是否在新选项卡中打开加密后的文件?", QMessageBox.Yes | QMessageBox.No) if ret == QMessageBox.Yes: self.open_local_file(file_path) except Exception as e: debug_error(f"转换失败: {e}") QMessageBox.critical(self, "转换失败", str(e)) def upload_current_tab(self): """上传当前文件到云端""" if not self.client.token: QMessageBox.warning(self, "提示", "请先登录") self.check_login() return idx = self.tabs.currentIndex() if idx < 0 or idx >= len(self.editor_tabs): return tab = self.editor_tabs[idx] if tab.is_new or not tab.file_path: QMessageBox.warning(self, "提示", "请先保存文件到本地") return if tab.modified: ret = QMessageBox.question(self, "提示", "文件未保存,是否先保存?", QMessageBox.Yes | QMessageBox.No) if ret == QMessageBox.Yes: self.save_current_tab() folder, ok = QInputDialog.getText(self, "上传到云端", "请输入云端目录路径 (默认为 /):", text="/") if not ok: return if not folder: folder = "/" res = self.client.upload_file(tab.file_path, folder) if res: QMessageBox.information(self, "成功", "文件已上传到云端") else: QMessageBox.warning(self, "错误", "上传失败") def show_cloud_manager(self): """显示云端管理器""" if not self.client.token: QMessageBox.warning(self, "提示", "请先登录") self.check_login() return dialog = CloudFileTreeDialog(self.client, self) dialog.exec_() def on_tab_changed(self, index): """当前选项卡改变时的回调""" if index >= 0 and index < len(self.editor_tabs): self.current_editor = self.editor_tabs[index] else: self.current_editor = None def close_tab(self, index): """关闭指定索引的选项卡""" if index < 0 or index >= len(self.editor_tabs): return tab = self.editor_tabs[index] if tab.modified: ret = QMessageBox.question(self, "提示", "文件已修改,是否保存?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) if ret == QMessageBox.Yes: self.save_current_tab() elif ret == QMessageBox.Cancel: return self.tabs.removeTab(index) self.editor_tabs.pop(index) for i, t in enumerate(self.editor_tabs): t.index = i current_idx = self.tabs.currentIndex() if current_idx >= 0 and current_idx < len(self.editor_tabs): self.current_editor = self.editor_tabs[current_idx] else: self.current_editor = None def about(self): """关于对话框""" QMessageBox.about(self, "关于云笔记", "云笔记 v3.0\n\n" "功能特性:\n" "• 支持 .txt 文本文件\n" "• 支持 .bjb 加密笔记\n" "• 云端同步功能\n" "• 多选项卡编辑\n" "• 加密当前文件\n" "• TXT转BJB加密\n" "• 支持高强度/低强度密码模式\n" "• 端到端加密:服务器不存储私钥\n\n" "加密模式:\n" "1. RSA-4096 + AES-256-GCM\n" "2. X25519 + AES-256-GCM\n\n" "密钥方式:\n" "• 默认加密:全局密钥对\n" "• 安全加密:单独生成密钥对\n\n" "快捷键:\n" "• Ctrl+N: 新建文本\n" "• Ctrl+Shift+N: 新建加密笔记\n" "• Ctrl+E: 加密当前文件\n" "• Ctrl+Shift+S: 另存为加密笔记\n" "• Ctrl+S: 保存\n" "• Ctrl+O: 打开文件\n\n" f"密钥存储位置:\n" f"• 用户目录: {USER_KEY_DIR}\n" f"• 程序目录: {APP_KEY_DIR}") def main(): """主函数""" if DEBUG: debug_log("=" * 50) debug_log("云笔记应用程序启动") debug_log("=" * 50) app = QApplication(sys.argv) app.setStyle('Fusion') ensure_global_keys() window = MainWindow() window.show() sys.exit(app.exec_()) if __name__ == "__main__": main()