Files
cloudnote/App.py
T
dvsxt cfd8a62ece upload: 上传文件
Signed-off-by: dvsxt <me@dvsyun.top>
2026-06-01 17:39:59 +08:00

2100 lines
85 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
云笔记应用程序
支持本地和云端文件管理,支持文本文件(.txt)和加密笔记(.bjb)
"""
import sys
import os
import json
import tempfile
import uuid
import hashlib
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"):
"""调试日志函数 - 通过全局变量DEBUG控制"""
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
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)
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():
key_path, source = get_key_path(KEY_FILES['rsa_private'])
if not os.path.exists(key_path):
ensure_global_keys()
key_path, source = get_key_path(KEY_FILES['rsa_private'])
with open(key_path, 'rb') as f:
return f.read()
def get_global_rsa_public():
key_path, source = get_key_path(KEY_FILES['rsa_public'])
if not os.path.exists(key_path):
ensure_global_keys()
key_path, source = get_key_path(KEY_FILES['rsa_public'])
with open(key_path, 'rb') as f:
return f.read()
def get_global_x25519_private():
key_path, source = get_key_path(KEY_FILES['x25519_private'])
if not os.path.exists(key_path):
ensure_global_keys()
key_path, source = get_key_path(KEY_FILES['x25519_private'])
with open(key_path, 'rb') as f:
return f.read()
def get_global_x25519_public():
key_path, source = get_key_path(KEY_FILES['x25519_public'])
if not os.path.exists(key_path):
ensure_global_keys()
key_path, source = 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')
except Exception as e:
debug_error(f"检查文件密码状态失败: {e}")
return False, 'unknown'
@staticmethod
def encrypt_content(content: str, algorithm: str, key_mode: str, password: str = None, single_private=None, single_public=None):
if DEBUG:
debug_log(f"开始加密 - 算法: {algorithm}, 密钥模式: {key_mode}, 密码保护: {password is not None}")
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)
else:
encrypted = encrypt_text_aes_rsa(data, pub_pem, add_header=True)
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)
else:
encrypted = encrypt_text_aes_rsa(data, public_key, add_header=True)
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:
if key_mode == 'global':
pub_raw = get_global_x25519_public()
if password:
encrypted = encrypt_text_aes_x25519(data, pub_raw, password, add_header=True)
else:
encrypted = encrypt_text_aes_x25519(data, pub_raw, add_header=True)
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)
else:
encrypted = encrypt_text_aes_x25519(data, public_key, add_header=True)
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):
if DEBUG:
debug_log(f"开始解密文件: {file_path}")
debug_log(f"密码提供: {password is not None}, 单独私钥提供: {single_private is not None}")
try:
with open(file_path, 'rb') as f:
data = f.read()
# 移除安全信息头,获取纯加密数据(bytes类型)
clean_data = remove_security_info(data)
# 将bytes转换为字符串用于JSON解析
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 mode in ['aes_rsa', 'aes_rsa_password_protected']:
if single_key:
if single_private is None:
raise ValueError("此文件使用单独RSA密钥加密,请选择对应的私钥文件(.key)")
priv = single_private
else:
priv = get_global_rsa_private()
if has_password:
if password is None:
raise ValueError("此文件受密码保护,请输入密码")
# 重要:传入原始bytes数据,不要重新编码
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("此文件使用单独X25519密钥加密,请选择对应的私钥文件(.key)")
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}")
# decrypted 已经是 bytes 类型,直接返回
if DEBUG:
debug_log(f"解密成功,解密后数据大小: {len(decrypted)}字节")
return decrypted
except Exception as e:
debug_error(f"解密过程异常: {e}")
raise
# ==================== 云端API客户端 ====================
class CloudClient:
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):
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
# ==================== 通用解密函数(本地和云端共用)====================
def decrypt_bjb_file(file_path, parent_window):
"""
解密.bjb文件的通用函数
返回: (content, success)
"""
if DEBUG:
debug_log(f"通用解密函数: {file_path}")
try:
# 检查是否有密钥文件
key_file = file_path + '.key'
single_private = None
if os.path.exists(key_file):
if DEBUG:
debug_log(f"发现密钥文件: {key_file}")
with open(key_file, 'rb') as f:
single_private = f.read()
if DEBUG:
debug_log(f"密钥文件读取成功,长度: {len(single_private)}字节")
# 检查文件是否有密码保护
has_password, mode = EncryptedNoteHandler.check_file_has_password(file_path)
if DEBUG:
debug_log(f"文件密码状态: has_password={has_password}, mode={mode}")
password = None
# 如果有密码保护,弹出密码输入框
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, "输入密码",
f"此文件受密码保护,请输入密码 (尝试 {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
)
content = decrypted.decode('utf-8')
success = True
if DEBUG:
debug_log("密码解密成功")
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"密码错误,还剩 {max_attempts - attempts} 次尝试")
else:
QMessageBox.warning(parent_window, "解密失败",
"多次密码错误,无法解密文件")
return None, False
else:
attempts += 1
QMessageBox.warning(parent_window, "密码为空", "密码不能为空")
if not success:
return None, False
else:
# 无密码保护,直接解密
if DEBUG:
debug_log("无密码保护,直接解密")
decrypted = EncryptedNoteHandler.decrypt_file(
file_path, password=None, single_private=single_private
)
content = decrypted.decode('utf-8')
return content, True
except Exception as e:
debug_error(f"解密失败: {e}")
QMessageBox.critical(parent_window, "解密失败", str(e))
return None, False
# ==================== 登录对话框 ====================
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):
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):
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, 650)
self.selected_algorithm = 'rsa'
self.selected_key_mode = 'global'
self.use_password = False
self.custom_password = None
self.single_private = None
self.single_public = None
self.setup_ui()
def setup_ui(self):
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()
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)
group3.setLayout(group3_layout)
layout.addWidget(group3)
info_label = QLabel("说明:\n• 默认加密:使用全局密钥对,所有文件共用同一密钥\n• 安全加密:每个文件独立生成密钥对,私钥保存为 .key 文件\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_password_toggled(self, checked):
if DEBUG:
debug_log(f"密码保护开关: {checked}")
self.password_input.setEnabled(checked)
self.confirm_input.setEnabled(checked)
self.use_password = checked
def check_password_strength(self):
pwd = self.password_input.text()
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;")
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}")
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 not is_password_strong(pwd):
QMessageBox.warning(self, "密码强度不足", "密码必须至少16位,包含大小写字母、数字和特殊字符")
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,
'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, 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.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):
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 "未启用"
info_layout.addWidget(QLabel(f"🔐 加密算法: {algo_text} + AES-256-GCM"))
info_layout.addWidget(QLabel(f"🔑 密钥方式: {key_text}"))
info_layout.addWidget(QLabel(f"🔒 密码保护: {pwd_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
)
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):
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):
"""下载并打开云端文件 - 修复:使用通用解密函数"""
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}")
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:
# 加密文件使用通用解密函数
content, success = decrypt_bjb_file(local_path, self)
if success and content is not None:
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:
QMessageBox.warning(self, "错误", "解密失败")
else:
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, 550)
self.selected_algorithm = 'rsa'
self.selected_key_mode = 'global'
self.use_password = False
self.custom_password = None
self.single_private = None
self.single_public = None
self.setup_ui()
def setup_ui(self):
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()
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)
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_password_toggled(self, checked):
if DEBUG:
debug_log(f"密码保护开关: {checked}")
self.password_input.setEnabled(checked)
self.confirm_input.setEnabled(checked)
self.use_password = checked
def check_password_strength(self):
pwd = self.password_input.text()
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;")
def accept_config(self):
if DEBUG:
debug_log("开始获取加密配置")
if self.radio_rsa.isChecked():
self.selected_algorithm = 'rsa'
else:
self.selected_algorithm = 'x25519'
if self.radio_global.isChecked():
self.selected_key_mode = 'global'
else:
self.selected_key_mode = 'single'
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 not is_password_strong(pwd):
QMessageBox.warning(self, "密码强度不足", "密码必须至少16位,包含大小写字母、数字和特殊字符")
return
self.custom_password = pwd
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()
self.accept()
def get_config(self):
return {
'algorithm': self.selected_algorithm,
'key_mode': self.selected_key_mode,
'use_password': self.use_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):
"""打开本地文件对话框 - 只支持.bjb和.txt"""
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)
else:
if DEBUG:
debug_log("用户取消选择")
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':
# 使用通用解密函数
content, success = decrypt_bjb_file(file_path, self)
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)
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):
return
tab = self.editor_tabs[idx]
content = tab.get_content()
if tab.is_new:
self.save_as_current_tab()
return
if tab.file_path and os.path.exists(tab.file_path):
try:
if tab.file_type == 'txt':
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)
else:
if hasattr(tab, 'encrypt_config') and tab.encrypt_config:
config = tab.encrypt_config
encrypted_data, _, _ = EncryptedNoteHandler.encrypt_content(
content, config['algorithm'], config['key_mode'],
config.get('custom_password'),
config.get('single_private'),
config.get('single_public')
)
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)
else:
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')
)
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)
else:
return
except Exception as e:
debug_error(f"保存失败: {e}")
QMessageBox.critical(self, "保存失败", str(e))
else:
self.save_as_current_tab()
def save_as_current_tab(self):
idx = self.tabs.currentIndex()
if idx < 0 or idx >= len(self.editor_tabs):
return
tab = self.editor_tabs[idx]
content = tab.get_content()
if tab.file_type == 'txt':
file_path, _ = QFileDialog.getSaveFileName(self, "保存文件", "", "文本文件 (*.txt)")
if not file_path:
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()
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:
if hasattr(tab, 'encrypt_config') and tab.encrypt_config:
config = tab.encrypt_config
else:
dialog = NewEncryptedFileDialog(self)
if dialog.exec_() != QDialog.Accepted:
return
config = dialog.get_config()
tab.encrypt_config = config
save_dialog = SaveEncryptedFileDialog(
content, config['algorithm'], config['key_mode'],
config['use_password'], 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()
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')
)
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')
)
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, "关于云笔记",
"云笔记 v2.0\n\n"
"功能特性:\n"
"• 支持 .txt 文本文件\n"
"• 支持 .bjb 加密笔记\n"
"• 云端同步功能\n"
"• 多选项卡编辑\n"
"• 加密当前文件\n"
"• TXT转BJB加密\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()