[Python] 纯文本查看 复制代码
import os
import sqlite3
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog, filedialog
import hashlib
import base64
import random
import string
import pyperclip
import webbrowser
from datetime import datetime, timedelta
import re
import threading
import time
import re
import csv
import json
class ShortcutManager:
def __init__(self, db_path):
self.db_path = db_path
self.default_shortcuts = {
'add_password': 'Alt+Q',
'view_password': 'Alt+W',
'edit_password': 'Alt+E',
'delete_password': 'Alt+D',
'generate_password': 'Alt+G',
'search': 'Alt+F',
'lock_app': 'Alt+L',
'unlock_app': 'F3',
'import_csv': 'Alt+I',
'export_csv': 'Alt+O',
'reset_search': 'Alt+R',
'refresh': 'F5',
'copy_password': 'Alt+C',
'toggle_floating': 'F1'
}
self.current_shortcuts = self.load_shortcuts()
def load_shortcuts(self):
"""从数据库加载快捷键设置"""
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT name, value FROM settings WHERE name LIKE "shortcut_%"')
results = c.fetchall()
conn.close()
shortcuts = {}
for name, value in results:
action = name.replace('shortcut_', '')
shortcuts[action] = value
# 合并默认快捷键(如果数据库中没有设置)
for action, default_key in self.default_shortcuts.items():
if action not in shortcuts:
shortcuts[action] = default_key
return shortcuts
except Exception as e:
return self.default_shortcuts.copy()
def save_shortcuts(self):
"""保存快捷键设置到数据库"""
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
for action, shortcut in self.current_shortcuts.items():
setting_name = f'shortcut_{action}'
c.execute('INSERT OR REPLACE INTO settings (name, value) VALUES (?, ?)',
(setting_name, shortcut))
conn.commit()
conn.close()
return True
except Exception as e:
return False
def get_shortcut(self, action):
"""获取指定动作的快捷键"""
return self.current_shortcuts.get(action, '')
def set_shortcut(self, action, shortcut):
"""设置指定动作的快捷键"""
self.current_shortcuts[action] = shortcut
def reset_to_defaults(self):
"""重置为默认快捷键"""
self.current_shortcuts = self.default_shortcuts.copy()
def check_conflict(self, new_shortcut, exclude_action=None):
"""检查快捷键冲突"""
conflicts = []
for action, shortcut in self.current_shortcuts.items():
if action != exclude_action and shortcut == new_shortcut:
conflicts.append(action)
return conflicts
def format_shortcut_display(self, shortcut):
"""格式化快捷键显示"""
if not shortcut:
return ""
return shortcut
def format_shortcut_binding(self, shortcut):
"""格式化快捷键绑定"""
if not shortcut:
return ""
# 将显示格式转换为Tkinter绑定格式
binding = shortcut.lower()
# 处理功能键
if binding.startswith('f') and binding[1:].isdigit():
return f'<{binding.upper()}>'
# 处理组合键
parts = binding.split('+')
if len(parts) == 1:
# 单个键
return f'<{parts[0]}>'
# 多个修饰键
result = '<'
for i, part in enumerate(parts[:-1]):
if part == 'ctrl':
result += 'Control-'
elif part == 'alt':
result += 'Alt-'
elif part == 'shift':
result += 'Shift-'
# 添加最后的键
result += parts[-1] + '>'
return result
class PasswordStrengthChecker:
"""密码强度检测器"""
@staticmethod
def check_strength(password):
"""检查密码强度,返回强度等级和详细信息"""
if not password:
return 0, "密码不能为空", "#dc3545"
score = 0
feedback = []
# 长度检查
if len(password) >= 12:
score += 25
elif len(password) >= 8:
score += 15
feedback.append("建议密码长度至少12位")
else:
feedback.append("密码太短,至少需要8位")
# 字符类型检查
has_lower = bool(re.search(r'[a-z]', password))
has_upper = bool(re.search(r'[A-Z]', password))
has_digit = bool(re.search(r'\d', password))
has_special = bool(re.search(r'[!@#$%^&*()_+\-=\[\]{};\':"\\|,.<>\/?]', password))
char_types = sum([has_lower, has_upper, has_digit, has_special])
score += char_types * 15
if not has_lower:
feedback.append("缺少小写字母")
if not has_upper:
feedback.append("缺少大写字母")
if not has_digit:
feedback.append("缺少数字")
if not has_special:
feedback.append("缺少特殊字符")
# 重复字符检查
if len(set(password)) < len(password) * 0.7:
score -= 10
feedback.append("重复字符过多")
# 常见模式检查
common_patterns = ['123', 'abc', 'qwe', 'password', '000', '111']
for pattern in common_patterns:
if pattern.lower() in password.lower():
score -= 15
feedback.append(f"包含常见模式: {pattern}")
break
# 确定强度等级和颜色
if score >= 80:
level = "很强"
color = "#28a745"
elif score >= 60:
level = "强"
color = "#17a2b8"
elif score >= 40:
level = "中等"
color = "#ffc107"
elif score >= 20:
level = "弱"
color = "#fd7e14"
else:
level = "很弱"
color = "#dc3545"
return score, level, color, feedback
class AutoLockManager:
"""自动锁定管理器"""
def __init__(self, timeout_minutes=1500):
self.timeout_minutes = timeout_minutes
self.last_activity = time.time()
self.is_locked = False
self.lock_callback = None
self.timer_thread = None
self.running = False
def start_monitoring(self, lock_callback):
"""开始监控用户活动"""
self.lock_callback = lock_callback
self.running = True
self.timer_thread = threading.Thread(target=self._monitor_activity, daemon=True)
self.timer_thread.start()
def stop_monitoring(self):
"""停止监控"""
self.running = False
def update_activity(self):
"""更新最后活动时间"""
self.last_activity = time.time()
self.is_locked = False
def _monitor_activity(self):
"""监控活动的后台线程"""
while self.running:
if not self.is_locked:
idle_time = time.time() - self.last_activity
if idle_time > self.timeout_minutes * 60:
self.is_locked = True
if self.lock_callback:
self.lock_callback()
time.sleep(30) # 每30秒检查一次
class PasswordManager:
def __init__(self, db_path='passwords.db', key_path=None):
self.db_path = db_path
# 设置默认密钥文件路径到运行目录
if key_path is None:
self.key_path = os.path.join(os.getcwd(), 'secret.key')
else:
self.key_path = key_path
# 设置配置文件路径到运行目录
self.config_path = os.path.join(os.getcwd(), 'config.dat')
self.key = None
self.salt = None
self.iterations = 100000
self.master_password_hash = None
self.is_locked = False
self.auto_lock_manager = AutoLockManager()
self.strength_checker = PasswordStrengthChecker()
self.shortcut_manager = None # 延迟初始化
self.setup()
def setup(self):
"""初始化密码管理器"""
# 检查数据库和密钥文件状态
db_exists = os.path.exists(self.db_path)
key_exists = os.path.exists(self.key_path)
# 首次启动检测 - 数据库文件不存在,直接生成密钥
if not db_exists:
return self.first_time_generate_key()
# 数据库存在的情况下,确保数据库结构完整
self.create_database()
# 清理数据库中过时的加密参数
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('DELETE FROM settings WHERE name IN (?, ?)', ('encryption_salt', 'encryption_iterations'))
conn.commit()
conn.close()
except Exception as e:
pass
# 检查密钥文件是否存在
if not key_exists:
self.handle_missing_key_file()
return False # 返回False表示初始化未完成
# 检查是否有保存的自动登录配置
if self.load_saved_config():
# 初始化快捷键管理器
self.shortcut_manager = ShortcutManager(self.db_path)
messagebox.showinfo('欢迎回来', '自动登录成功!')
return True
else:
# 数据库和密钥文件都存在,进行正常的主密码验证
self.verify_master_password()
# 初始化快捷键管理器
self.shortcut_manager = ShortcutManager(self.db_path)
return True
def first_time_generate_key(self):
"""首次启动直接生成密钥"""
try:
# 先创建数据库
self.create_database()
# 设置主密码
master_password = self.set_master_password()
if not master_password:
exit()
# 生成密钥文件
self.generate_key()
# 使用主密码哈希派生密钥
self.load_key_with_hash(self.master_password_hash)
# 创建密钥验证数据
self.create_key_validation()
# 初始化快捷键管理器
self.shortcut_manager = ShortcutManager(self.db_path)
# 显示成功消息
messagebox.showinfo('设置完成',
'🎉 密码管理器初始化完成!\n\n'
'✅ 数据库已创建\n'
'✅ 密钥文件已生成\n'
'✅ 使用军用级AES-GCM加密保护\n\n'
'现在您可以开始添加密码了!')
return True
except Exception as e:
messagebox.showerror('设置失败', f'初始化失败:{str(e)}\n\n程序将退出。')
exit()
def first_time_setup(self):
"""首次启动设置流程"""
# 显示欢迎对话框
welcome_result = self.show_welcome_dialog()
if not welcome_result:
exit()
# 设置主密码
master_password = self.show_first_time_password_dialog()
if not master_password:
exit()
try:
# 创建数据库
self.create_database()
# 生成密钥文件
self.generate_key()
# 计算并保存主密码哈希
self.master_password_hash = hashlib.sha256(master_password.encode()).hexdigest()
# 保存主密码哈希到数据库
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('INSERT OR REPLACE INTO settings (name, value) VALUES (?, ?)',
('master_password_hash', self.master_password_hash))
conn.commit()
conn.close()
# 使用主密码哈希派生密钥
self.load_key_with_hash(self.master_password_hash)
# 创建密钥验证数据
self.create_key_validation()
# 保存自动登录配置
self.save_config(master_password)
# 初始化快捷键管理器
self.shortcut_manager = ShortcutManager(self.db_path)
# 显示成功消息
messagebox.showinfo('设置完成',
'🎉 密码管理器设置完成!\n\n'
'✅ 数据库已创建\n'
'✅ 密钥文件已生成\n'
'✅ 使用军用级AES-GCM加密保护\n\n'
'⚠️ 请妥善保管您的主密码,遗失后无法恢复!')
return True
except Exception as e:
messagebox.showerror('设置失败', f'初始化失败:{str(e)}\n\n程序将退出。')
exit()
def show_welcome_dialog(self):
"""显示首次启动欢迎对话框"""
dialog = tk.Toplevel()
dialog.title('欢迎使用安全密码管理器')
dialog.geometry('500x400')
dialog.resizable(False, False)
dialog.transient()
dialog.grab_set()
dialog.configure(bg='#f8f9fa')
# 居中显示
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
# 主框架
main_frame = ttk.Frame(dialog, padding='30')
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题
title_label = ttk.Label(main_frame,
text='🔐 欢迎使用安全密码管理器 v1.0',
font=('Microsoft YaHei UI', 18, 'bold'),
foreground='#2c3e50')
title_label.pack(pady=(0, 20))
# 欢迎信息
welcome_text = """感谢您选择安全密码管理器!
这是您第一次使用本软件,我们将为您进行初始设置。
🛡️ 安全特性:
• 采用军用级 AES-GCM 加密算法
• 本地存储,数据完全掌控
• 密钥派生使用 PBKDF2-HMAC-SHA256
🎨 功能亮点:
• 8种精美主题,个性化体验
• 自定义快捷键,提升效率
• 智能密码强度检测
• 自动锁定保护隐私
接下来,您需要设置一个主密码来保护您的数据。"""
info_label = ttk.Label(main_frame,
text=welcome_text,
font=('Microsoft YaHei UI', 10),
justify=tk.LEFT,
foreground='#34495e')
info_label.pack(pady=(0, 30))
result = {'continue': False}
def continue_setup():
result['continue'] = True
dialog.destroy()
def cancel_setup():
if messagebox.askyesno('确认退出', '确定要退出设置吗?\n\n退出后程序将关闭。'):
result['continue'] = False
dialog.destroy()
# 按钮框架
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill=tk.X, pady=(20, 0))
ttk.Button(btn_frame, text='🚀 开始设置', command=continue_setup, width=20).pack(side=tk.RIGHT, padx=(10, 0))
ttk.Button(btn_frame, text='❌ 退出', command=cancel_setup, width=15).pack(side=tk.RIGHT)
# 设置关闭事件
dialog.protocol("WM_DELETE_WINDOW", cancel_setup)
dialog.wait_window()
return result['continue']
def show_first_time_password_dialog(self):
"""显示首次设置主密码对话框(简化版)"""
dialog = tk.Toplevel()
dialog.title('首次使用 - 设置主密码')
dialog.geometry('450x350')
dialog.resizable(False, False)
dialog.transient()
dialog.grab_set()
dialog.configure(bg='#f8f9fa')
# 居中显示
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
# 主框架
main_frame = ttk.Frame(dialog, padding='30')
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题
title_label = ttk.Label(main_frame,
text='🔑 设置主密码',
font=('Microsoft YaHei UI', 16, 'bold'),
foreground='#2c3e50')
title_label.pack(pady=(0, 20))
# 说明文字
info_text = """主密码是保护您所有数据的关键,请务必:
• 使用至少8位字符
• 包含大小写字母、数字和特殊字符
• 避免使用个人信息(姓名、生日等)
⚠️ 重要提醒:主密码遗失后无法恢复,请务必牢记!"""
ttk.Label(main_frame,
text=info_text,
font=('Microsoft YaHei UI', 10),
justify=tk.LEFT,
foreground='#7f8c8d').pack(pady=(0, 20))
# 密码输入区域
password_frame = ttk.LabelFrame(main_frame, text='密码设置', padding='15')
password_frame.pack(fill=tk.X, pady=(0, 20))
# 主密码输入
ttk.Label(password_frame, text='主密码:', font=('Microsoft YaHei UI', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
password_var = tk.StringVar()
password_entry = ttk.Entry(password_frame, textvariable=password_var, show='*',
font=('Microsoft YaHei UI', 12), width=30)
password_entry.pack(fill=tk.X, pady=(0, 10))
# 确认密码输入
ttk.Label(password_frame, text='确认密码:', font=('Microsoft YaHei UI', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
confirm_var = tk.StringVar()
confirm_entry = ttk.Entry(password_frame, textvariable=confirm_var, show='*',
font=('Microsoft YaHei UI', 12), width=30)
confirm_entry.pack(fill=tk.X, pady=(0, 10))
result = {'password': None}
def validate_and_confirm():
password = password_var.get()
confirm = confirm_var.get()
# 验证密码长度
if len(password) < 8:
messagebox.showerror('密码不符合要求', '密码长度至少需要8位字符!')
password_entry.focus()
return
# 验证密码一致性
if password != confirm:
messagebox.showerror('密码不一致', '两次输入的密码不一致,请重新输入!')
confirm_entry.delete(0, tk.END)
confirm_entry.focus()
return
result['password'] = password
dialog.destroy()
def cancel_setup():
if messagebox.askyesno('确认取消', '确定要取消设置吗?\n\n取消后程序将退出。'):
result['password'] = None
dialog.destroy()
# 按钮框架
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill=tk.X, pady=(20, 0))
ttk.Button(btn_frame, text='✅ 确认设置', command=validate_and_confirm, width=20).pack(side=tk.RIGHT, padx=(10, 0))
ttk.Button(btn_frame, text='❌ 取消', command=cancel_setup, width=15).pack(side=tk.RIGHT)
# 绑定回车键
password_entry.bind('<Return>', lambda e: confirm_entry.focus())
confirm_entry.bind('<Return>', lambda e: validate_and_confirm())
password_entry.focus()
# 设置关闭事件
dialog.protocol("WM_DELETE_WINDOW", cancel_setup)
dialog.wait_window()
return result['password']
def save_config(self, master_password):
"""保存配置到运行目录(加密存储)"""
try:
# 生成配置数据
config_data = {
'master_password_hash': hashlib.sha256(master_password.encode()).hexdigest(),
'salt': base64.b64encode(self.salt).decode(),
'iterations': self.iterations,
'created_time': datetime.now().isoformat()
}
# 使用机器ID加密配置数据
machine_id = self._get_machine_id()
config_json = json.dumps(config_data)
encrypted_config = self._encrypt_config(config_json, machine_id)
with open(self.config_path, 'wb') as f:
f.write(encrypted_config)
messagebox.showinfo('信息', '配置已安全保存到运行目录,下次启动将自动登录')
except Exception as e:
messagebox.showerror('错误', f'保存配置失败: {str(e)}')
def _encrypt_config(self, data, password):
"""加密配置数据"""
# 生成随机盐和IV
salt = os.urandom(16)
iv = os.urandom(12)
# 从密码派生密钥
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
)
key = kdf.derive(password.encode())
# 使用AES-GCM加密
cipher = Cipher(algorithms.AES(key), modes.GCM(iv))
encryptor = cipher.encryptor()
ciphertext = encryptor.update(data.encode()) + encryptor.finalize()
# 返回 salt + iv + tag + ciphertext
return salt + iv + encryptor.tag + ciphertext
def _decrypt_config(self, encrypted_data, password):
"""解密配置数据"""
# 分离各部分
salt = encrypted_data[:16]
iv = encrypted_data[16:28]
tag = encrypted_data[28:44]
ciphertext = encrypted_data[44:]
# 从密码派生密钥
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=salt,
iterations=100000,
)
key = kdf.derive(password.encode())
# 解密
cipher = Cipher(algorithms.AES(key), modes.GCM(iv, tag))
decryptor = cipher.decryptor()
return (decryptor.update(ciphertext) + decryptor.finalize()).decode()
def load_saved_config(self):
"""加载保存的配置(解密)"""
try:
if not os.path.exists(self.config_path):
return False
# 尝试使用机器特征作为密码解密
machine_id = self._get_machine_id()
with open(self.config_path, 'rb') as f:
encrypted_data = f.read()
config_json = self._decrypt_config(encrypted_data, machine_id)
config_data = json.loads(config_json)
self.master_password_hash = config_data['master_password_hash']
# 从密钥文件加载盐值和迭代次数
if not os.path.exists(self.key_path):
return False
with open(self.key_path, 'rb') as f:
self.salt = base64.b64decode(f.readline().strip())
self.iterations = int(f.readline().strip())
# 使用主密码哈希派生密钥(与正常登录流程一致)
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=self.salt,
iterations=self.iterations,
)
self.key = kdf.derive(self.master_password_hash.encode())
# 验证密钥是否与数据库匹配
if not self.validate_key():
# 密钥验证失败,删除配置文件强制重新验证
try:
os.remove(self.config_path)
except:
pass
return False
return True
except Exception as e:
# 如果配置文件损坏,删除它
try:
os.remove(self.config_path)
except:
pass
return False
def _get_machine_id(self):
"""获取机器特征ID用于配置加密"""
try:
# 使用多个系统特征生成唯一ID
import platform
machine_info = f"{platform.node()}{platform.machine()}{platform.processor()}"
return hashlib.sha256(machine_info.encode()).hexdigest()[:32]
except:
# 如果获取失败,使用固定字符串
return "default_machine_id_for_config"
def handle_missing_key_file(self):
"""处理密钥文件丢失的情况"""
# 显示警告对话框
warning_msg = """
⚠️ 检测到密钥文件丢失!
数据库文件存在但密钥文件 (secret.key) 丢失。
这可能是由于以下原因:
• 密钥文件被意外删除
• 程序目录被移动
• 文件权限问题
请选择恢复方式:
"""
# 创建恢复对话框
recovery_dialog = tk.Toplevel()
recovery_dialog.title('🔑 密钥文件恢复')
recovery_dialog.geometry('500x400')
recovery_dialog.resizable(False, False)
recovery_dialog.grab_set()
recovery_dialog.configure(bg='#fff3cd')
# 居中显示
recovery_dialog.update_idletasks()
width = recovery_dialog.winfo_width()
height = recovery_dialog.winfo_height()
x = (recovery_dialog.winfo_screenwidth() // 2) - (width // 2)
y = (recovery_dialog.winfo_screenheight() // 2) - (height // 2)
recovery_dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
frame = ttk.Frame(recovery_dialog, padding='20')
frame.pack(fill=tk.BOTH, expand=True)
# 警告图标和标题
ttk.Label(frame, text='⚠️', font=('Microsoft YaHei UI', 48), foreground='#856404').pack(pady=(0, 10))
ttk.Label(frame, text='密钥文件丢失', font=('Microsoft YaHei UI', 16, 'bold'), foreground='#856404').pack(pady=5)
# 说明文本
text_widget = tk.Text(frame, wrap=tk.WORD, height=8, width=50, font=('Microsoft YaHei UI', 10))
text_widget.insert(tk.END, warning_msg)
text_widget.config(state=tk.DISABLED, bg='#fff3cd', relief=tk.FLAT)
text_widget.pack(pady=10, fill=tk.BOTH, expand=True)
# 按钮框架
btn_frame = ttk.Frame(frame)
btn_frame.pack(pady=20)
def try_master_password_recovery():
"""尝试使用主密码恢复"""
recovery_dialog.destroy()
self.master_password_recovery()
def restore_from_backup():
"""从备份恢复"""
file_path = filedialog.askopenfilename(
title="选择密钥文件备份",
filetypes=[("密钥文件", "*.key"), ("所有文件", "*.*")]
)
if file_path:
try:
import shutil
shutil.copy2(file_path, self.key_path)
# 验证恢复的密钥文件是否有效
if self.verify_restored_key():
messagebox.showinfo("恢复成功", "密钥文件已恢复!程序将重新启动。")
recovery_dialog.destroy()
self.setup() # 重新初始化
else:
messagebox.showerror("恢复失败", "恢复的密钥文件与数据库不匹配!\n请确认选择了正确的密钥文件备份。")
# 删除无效的密钥文件
try:
os.remove(self.key_path)
except:
pass
except Exception as e:
messagebox.showerror("恢复失败", f"无法恢复密钥文件:{str(e)}")
def create_new_database():
"""创建新数据库"""
confirm = messagebox.askyesno("确认操作",
"这将创建一个全新的数据库,原有数据将无法访问。\n确定要继续吗?")
if confirm:
try:
# 备份原数据库
backup_name = f"passwords_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.db"
import shutil
shutil.copy2(self.db_path, backup_name)
# 删除原数据库和配置
os.remove(self.db_path)
if os.path.exists(self.config_path):
os.remove(self.config_path)
messagebox.showinfo("备份完成", f"原数据库已备份为:{backup_name}")
recovery_dialog.destroy()
self.setup() # 重新初始化
except Exception as e:
messagebox.showerror("操作失败", f"无法创建新数据库:{str(e)}")
def close_recovery_dialog():
"""关闭恢复对话框并退出程序"""
recovery_dialog.destroy()
# 显示提示信息并退出程序
messagebox.showwarning("程序退出", "密钥文件丢失,无法访问现有数据。\n程序将退出。")
exit()
# 恢复选项按钮
ttk.Button(btn_frame, text='🔑 使用主密码恢复', command=try_master_password_recovery, width=20).pack(pady=5)
ttk.Button(btn_frame, text='📁 从备份恢复', command=restore_from_backup, width=20).pack(pady=5)
ttk.Button(btn_frame, text='🆕 创建新数据库', command=create_new_database, width=20).pack(pady=5)
ttk.Button(btn_frame, text='❌ 退出程序', command=close_recovery_dialog, width=20).pack(pady=5)
# 设置对话框关闭事件
recovery_dialog.protocol("WM_DELETE_WINDOW", close_recovery_dialog)
def master_password_recovery(self):
"""使用主密码尝试恢复密钥文件"""
# 从数据库获取主密码哈希
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT value FROM settings WHERE name = ?', ('master_password_hash',))
result = c.fetchone()
conn.close()
if not result:
messagebox.showerror('恢复失败', '数据库中未找到主密码信息!')
return
stored_hash = result[0]
# 创建密码输入对话框
password_dialog = tk.Toplevel()
password_dialog.title('🔑 主密码恢复')
password_dialog.geometry('400x250')
password_dialog.resizable(False, False)
password_dialog.grab_set()
password_dialog.configure(bg='#f8f9fa')
# 居中显示
password_dialog.update_idletasks()
width = password_dialog.winfo_width()
height = password_dialog.winfo_height()
x = (password_dialog.winfo_screenwidth() // 2) - (width // 2)
y = (password_dialog.winfo_screenheight() // 2) - (height // 2)
password_dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
frame = ttk.Frame(password_dialog, padding='30')
frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, text='🔑', font=('Microsoft YaHei UI', 32)).pack(pady=(0, 10))
ttk.Label(frame, text='密钥文件恢复', font=('Microsoft YaHei UI', 14, 'bold')).pack(pady=5)
ttk.Label(frame, text='请输入主密码以重新生成密钥文件', font=('Microsoft YaHei UI', 10)).pack(pady=5)
password_var = tk.StringVar()
password_entry = ttk.Entry(frame, textvariable=password_var, show='*', width=30, font=('Microsoft YaHei UI', 10))
password_entry.pack(pady=10)
password_entry.focus()
def attempt_recovery():
password = password_var.get()
if password and hashlib.sha256(password.encode()).hexdigest() == stored_hash:
try:
# 重新生成密钥文件
self.salt = os.urandom(16)
self.generate_key()
# 使用主密码哈希派生密钥
self.load_key_with_hash(stored_hash)
# 重新创建密钥验证数据
self.create_key_validation()
self.save_config(password)
messagebox.showinfo('恢复成功', '密钥文件已成功恢复!')
password_dialog.destroy()
except Exception as e:
messagebox.showerror('恢复失败', f'无法恢复密钥文件:{str(e)}')
else:
messagebox.showerror('密码错误', '主密码不正确,请重试!')
password_var.set('')
password_entry.focus()
password_entry.bind('<Return>', lambda e: attempt_recovery())
btn_frame = ttk.Frame(frame)
btn_frame.pack(pady=15)
ttk.Button(btn_frame, text='🔓 恢复', command=attempt_recovery, width=15).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text='❌ 取消', command=password_dialog.destroy, width=15).pack(side=tk.LEFT, padx=5)
except Exception as e:
messagebox.showerror('错误', f'无法访问数据库:{str(e)}')
def generate_key(self):
# 生成盐值
self.salt = os.urandom(16)
# 确保密钥目录存在
key_dir = os.path.dirname(self.key_path)
if not os.path.exists(key_dir):
os.makedirs(key_dir, exist_ok=True)
# 保存盐值和迭代次数到密钥文件
with open(self.key_path, 'wb') as f:
f.write(base64.b64encode(self.salt) + b'\n')
f.write(str(self.iterations).encode() + b'\n')
messagebox.showinfo('信息', '加密参数已初始化')
# 生成并保存密钥验证数据
self.create_key_validation()
def load_key(self, master_password):
# 从密钥文件加载盐值和迭代次数
try:
if not os.path.exists(self.key_path):
messagebox.showerror('错误', f'密钥文件不存在: {self.key_path}')
return False
with open(self.key_path, 'rb') as f:
self.salt = base64.b64decode(f.readline().strip())
self.iterations = int(f.readline().strip())
# 使用PBKDF2HMAC从主密码派生密钥
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=self.salt,
iterations=self.iterations,
)
self.key = kdf.derive(master_password.encode())
return True
except Exception as e:
messagebox.showerror('错误', f'加载加密参数失败: {str(e)}')
return False
def load_key_with_hash(self, master_password_hash):
"""使用主密码哈希派生密钥"""
try:
if not os.path.exists(self.key_path):
messagebox.showerror('错误', f'密钥文件不存在: {self.key_path}')
return False
with open(self.key_path, 'rb') as f:
self.salt = base64.b64decode(f.readline().strip())
self.iterations = int(f.readline().strip())
# 使用PBKDF2HMAC从主密码哈希派生密钥
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=self.salt,
iterations=self.iterations,
)
self.key = kdf.derive(master_password_hash.encode())
return True
except Exception as e:
messagebox.showerror('错误', f'加载加密参数失败: {str(e)}')
return False
def create_key_validation(self):
"""创建密钥验证数据"""
try:
if not self.key:
return False
# 生成密钥哈希
key_hash = hashlib.sha256(self.key).hexdigest()
# 创建验证数据(使用已知字符串加密)
validation_text = "PASSWORD_MANAGER_KEY_VALIDATION_2025"
validation_encrypted = self.encrypt_password(validation_text)
if validation_encrypted:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
# 清除旧的验证数据
c.execute('DELETE FROM key_validation')
# 插入新的验证数据
c.execute('INSERT INTO key_validation (key_hash, validation_data) VALUES (?, ?)',
(key_hash, validation_encrypted))
conn.commit()
conn.close()
return True
except Exception as e:
return False
def validate_key(self):
"""验证当前密钥是否与数据库匹配"""
try:
if not self.key:
return False
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT key_hash, validation_data FROM key_validation ORDER BY created_at DESC LIMIT 1')
result = c.fetchone()
if not result:
# 如果没有验证数据,尝试验证现有密码
c.execute('SELECT encrypted_password FROM passwords LIMIT 1')
password_result = c.fetchone()
conn.close()
if password_result:
# 尝试解密现有密码来验证密钥
try:
decrypted = self.decrypt_password(password_result[0])
if decrypted: # 如果能成功解密,说明密钥正确
# 创建验证数据
return self.create_key_validation()
else:
return False
except:
return False
else:
# 没有密码数据,创建验证数据
return self.create_key_validation()
else:
conn.close()
stored_key_hash, validation_encrypted = result
current_key_hash = hashlib.sha256(self.key).hexdigest()
# 检查密钥哈希是否匹配
if stored_key_hash != current_key_hash:
return False
# 尝试解密验证数据
validation_text = self.decrypt_password(validation_encrypted)
expected_text = "PASSWORD_MANAGER_KEY_VALIDATION_2025"
return validation_text == expected_text
except Exception as e:
return False
def verify_restored_key(self):
"""验证恢复的密钥文件是否与数据库匹配"""
try:
# 获取主密码哈希
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT value FROM settings WHERE name = ?', ('master_password_hash',))
result = c.fetchone()
conn.close()
if not result:
return False
master_password_hash = result[0]
# 尝试使用主密码哈希作为密码来加载密钥
temp_password = master_password_hash[:32] # 使用哈希的前32位作为临时密码
# 保存当前状态
old_key = self.key
old_salt = self.salt
old_iterations = self.iterations
try:
# 尝试加载恢复的密钥文件
with open(self.key_path, 'rb') as f:
self.salt = base64.b64decode(f.readline().strip())
self.iterations = int(f.readline().strip())
# 使用多种可能的密码尝试验证
for attempt_password in [master_password_hash, temp_password]:
try:
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=self.salt,
iterations=self.iterations,
)
self.key = kdf.derive(attempt_password.encode())
# 验证密钥
if self.validate_key():
return True
except:
continue
return False
except Exception:
return False
finally:
# 恢复原状态
self.key = old_key
self.salt = old_salt
self.iterations = old_iterations
except Exception as e:
return False
def create_database(self):
# 创建SQLite数据库和表
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
# 创建密码表
c.execute('''CREATE TABLE IF NOT EXISTS passwords
(id INTEGER PRIMARY KEY AUTOINCREMENT,
service TEXT NOT NULL,
url TEXT,
notes TEXT,
category TEXT DEFAULT '默认',
expires_at TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
# 创建密码历史表
c.execute('''CREATE TABLE IF NOT EXISTS password_history
(id INTEGER PRIMARY KEY AUTOINCREMENT,
password_id INTEGER,
encrypted_password TEXT NOT NULL,
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (password_id) REFERENCES passwords (id))''')
# 创建分类表
c.execute('''CREATE TABLE IF NOT EXISTS categories
(id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
color TEXT DEFAULT '#007bff',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
# 创建设置表
c.execute('''CREATE TABLE IF NOT EXISTS settings
(name TEXT PRIMARY KEY,
value TEXT NOT NULL)''')
# 创建密钥验证表
c.execute('''CREATE TABLE IF NOT EXISTS key_validation
(id INTEGER PRIMARY KEY,
key_hash TEXT NOT NULL,
validation_data TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
# 创建加密文本表
c.execute('''CREATE TABLE IF NOT EXISTS encrypted_texts
(id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
encrypted_content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)''')
# 添加默认分类
c.execute('''INSERT OR IGNORE INTO categories (name, color) VALUES
('默认', '#6c757d'),
('工作', '#007bff'),
('个人', '#28a745'),
('社交', '#17a2b8'),
('金融', '#ffc107'),
('购物', '#fd7e14')''')
# 检查并添加新列(用于数据库升级)
try:
c.execute('ALTER TABLE passwords ADD COLUMN category TEXT DEFAULT "默认"')
except sqlite3.OperationalError:
pass # 列已存在
try:
c.execute('ALTER TABLE passwords ADD COLUMN expires_at TIMESTAMP')
except sqlite3.OperationalError:
pass # 列已存在
conn.commit()
conn.close()
except Exception as e:
messagebox.showerror('错误', f'创建数据库失败: {str(e)}')
exit(1)
def set_master_password(self):
# 设置主密码
while True:
master_password = simpledialog.askstring('设置主密码', '请设置主密码 (至少8位,包含大小写字母和数字):', show='*')
if not master_password:
messagebox.showerror('错误', '主密码不能为空!')
continue
# 密码强度检查
if len(master_password) < 8:
messagebox.showerror('错误', '主密码长度至少为8位!')
continue
if not any(c.isupper() for c in master_password):
messagebox.showerror('错误', '主密码必须包含大写字母!')
continue
if not any(c.islower() for c in master_password):
messagebox.showerror('错误', '主密码必须包含小写字母!')
continue
if not any(c.isdigit() for c in master_password):
messagebox.showerror('错误', '主密码必须包含数字!')
continue
confirm_password = simpledialog.askstring('确认主密码', '请再次输入主密码:', show='*')
if master_password == confirm_password:
# 哈希主密码
self.master_password_hash = hashlib.sha256(master_password.encode()).hexdigest()
# 保存主密码哈希到数据库
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('INSERT OR REPLACE INTO settings (name, value) VALUES (?, ?)',
('master_password_hash', self.master_password_hash))
conn.commit()
conn.close()
messagebox.showinfo('成功', '主密码设置成功!')
return master_password
else:
messagebox.showerror('错误', '两次输入的密码不匹配!')
def verify_master_password(self):
# 验证主密码并派生加密密钥
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT value FROM settings WHERE name = ?', ('master_password_hash',))
result = c.fetchone()
conn.close()
if not result:
messagebox.showerror('错误', '未找到主密码信息,请重新设置!')
self.set_master_password()
return True
self.master_password_hash = result[0]
max_attempts = 3
attempts = 0
while attempts < max_attempts:
password = simpledialog.askstring('验证', '请输入主密码:', show='*')
if password and hashlib.sha256(password.encode()).hexdigest() == self.master_password_hash:
# 加载并派生密钥(使用主密码哈希)
if not self.load_key_with_hash(self.master_password_hash):
messagebox.showerror('错误', '密钥派生失败')
exit(1)
# 验证密钥是否与数据库匹配
if not self.validate_key():
messagebox.showerror('错误', '密钥文件与数据库不匹配!\n可能的原因:\n• 密钥文件已损坏\n• 使用了错误的密钥文件\n• 数据库文件已损坏\n\n请尝试从备份恢复密钥文件。')
exit(1)
# 保存配置以便下次自动登录
self.save_config(password)
return True
attempts += 1
remaining = max_attempts - attempts
if remaining > 0:
messagebox.showerror('错误', f'密码错误! 还剩 {remaining} 次尝试机会')
else:
messagebox.showerror('错误', '尝试次数过多,程序将退出')
exit(1)
def encrypt_password(self, password):
# 使用AES-GCM加密
try:
# 生成随机IV
iv = os.urandom(12)
# 创建加密器
cipher = Cipher(algorithms.AES(self.key), modes.GCM(iv))
encryptor = cipher.encryptor()
# 加密数据
ciphertext = encryptor.update(password.encode()) + encryptor.finalize()
# 返回IV + 标签 + 密文 (都进行base64编码以便存储)
return base64.b64encode(iv + encryptor.tag + ciphertext).decode()
except Exception as e:
messagebox.showerror('错误', f'加密失败: {str(e)}')
return None
def decrypt_password(self, encrypted_password):
# 使用AES-GCM解密
try:
# 解码base64数据
data = base64.b64decode(encrypted_password.encode())
# 分离IV、标签和密文
iv = data[:12]
tag = data[12:28]
ciphertext = data[28:]
# 创建解密器
cipher = Cipher(algorithms.AES(self.key), modes.GCM(iv, tag))
decryptor = cipher.decryptor()
# 解密数据
return (decryptor.update(ciphertext) + decryptor.finalize()).decode()
except Exception as e:
messagebox.showerror('错误', f'解密失败: {str(e)}')
return None
def decrypt_password_legacy(self, encrypted_password):
"""使用原始主密码派生的密钥解密(兼容性方法)"""
try:
# 保存当前密钥
current_key = self.key
# 尝试使用原始主密码派生密钥
if hasattr(self, 'master_password_hash'):
# 从数据库获取主密码哈希
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT value FROM settings WHERE name = ?', ('master_password_hash',))
result = c.fetchone()
conn.close()
if result:
master_password_hash = result[0]
# 尝试多种可能的密钥派生方式
for password_candidate in [master_password_hash, master_password_hash[:32]]:
try:
# 使用原始密码派生密钥
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32,
salt=self.salt,
iterations=self.iterations,
)
legacy_key = kdf.derive(password_candidate.encode())
# 临时使用legacy密钥
self.key = legacy_key
# 尝试解密
decrypted = self.decrypt_password(encrypted_password)
if decrypted:
# 恢复原密钥
self.key = current_key
return decrypted
except:
continue
# 恢复原密钥
self.key = current_key
return None
except Exception as e:
# 恢复原密钥
if 'current_key' in locals():
self.key = current_key
return None
def add_password(self, service, username, password, url='', notes=''):
# 添加新密码到数据库
if not all([service, username, password]):
messagebox.showerror('错误', '服务名、用户名和密码不能为空!')
return False
encrypted_password = self.encrypt_password(password)
if not encrypted_password:
return False
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
# 检查是否已存在相同服务和用户名的密码
c.execute('SELECT id FROM passwords WHERE service = ? AND username = ?',
(service, username))
if c.fetchone():
messagebox.showerror('错误', f'已存在{service}的{username}记录!')
conn.close()
return False
# 手动设置时间戳
from datetime import datetime
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
c.execute('INSERT INTO passwords (service, username, encrypted_password, url, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
(service, username, encrypted_password, url, notes, current_time, current_time))
conn.commit()
conn.close()
messagebox.showinfo('成功', '密码添加成功!')
return True
except Exception as e:
messagebox.showerror('错误', f'添加密码失败: {str(e)}')
return False
def get_password(self, service, username):
# 获取密码
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT encrypted_password FROM passwords WHERE service = ? AND username = ?',
(service, username))
result = c.fetchone()
conn.close()
if result:
# 尝试用当前密钥解密
decrypted = self.decrypt_password(result[0])
if decrypted:
return decrypted
# 如果失败,尝试用原始主密码派生的密钥解密(兼容性)
return self.decrypt_password_legacy(result[0])
return None
except Exception as e:
messagebox.showerror('错误', f'获取密码失败: {str(e)}')
return None
def update_password(self, password_id, service, username, new_password, url, notes):
# 更新密码
if new_password: # 只有当提供新密码时才加密
encrypted_password = self.encrypt_password(new_password)
if not encrypted_password:
return False
else:
# 如果没有提供新密码,获取当前加密密码
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT encrypted_password FROM passwords WHERE id = ?', (password_id,))
result = c.fetchone()
conn.close()
if not result:
messagebox.showerror('错误', '未找到密码记录!')
return False
encrypted_password = result[0]
except Exception as e:
messagebox.showerror('错误', f'获取当前密码失败: {str(e)}')
return False
try:
# 手动设置更新时间戳
from datetime import datetime
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('UPDATE passwords SET service = ?, username = ?, encrypted_password = ?, url = ?, notes = ?, updated_at = ? WHERE id = ?',
(service, username, encrypted_password, url, notes, current_time, password_id))
conn.commit()
affected = c.rowcount
conn.close()
if affected > 0:
messagebox.showinfo('成功', '记录更新成功!')
return True
else:
messagebox.showerror('错误', '未找到密码记录!')
return False
except Exception as e:
messagebox.showerror('错误', f'更新记录失败: {str(e)}')
return False
def delete_password(self, password_id):
# 删除密码
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('DELETE FROM passwords WHERE id = ?', (password_id,))
conn.commit()
affected = c.rowcount
conn.close()
if affected > 0:
messagebox.showinfo('成功', '密码删除成功!')
return True
else:
messagebox.showerror('错误', '未找到密码记录!')
return False
except Exception as e:
messagebox.showerror('错误', f'删除密码失败: {str(e)}')
return False
def get_all_passwords(self):
# 获取所有密码
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT id, service, username, url, notes, created_at, updated_at FROM passwords ORDER BY service')
results = c.fetchall()
conn.close()
return results
except Exception as e:
messagebox.showerror('错误', f'获取密码列表失败: {str(e)}')
return []
def search_passwords(self, keyword):
# 搜索密码
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
query = "SELECT id, service, username, url, notes, created_at, updated_at FROM passwords WHERE service LIKE ? OR username LIKE ? OR url LIKE ? OR notes LIKE ? ORDER BY service"
c.execute(query, (f'%{keyword}%', f'%{keyword}%', f'%{keyword}%', f'%{keyword}%'))
results = c.fetchall()
conn.close()
return results
except Exception as e:
messagebox.showerror('错误', f'搜索密码失败: {str(e)}')
return []
def generate_strong_password(self, length=16):
# 生成强密码
if length < 8:
length = 8
# 定义字符集
uppercase = string.ascii_uppercase
lowercase = string.ascii_lowercase
digits = string.digits
special_chars = '!@#$%^&*()_-+=[]{}|;:,.<>?'
# 确保包含每种类型的字符
password = [
random.choice(uppercase),
random.choice(lowercase),
random.choice(digits),
random.choice(special_chars)
]
# 填充剩余长度
all_chars = uppercase + lowercase + digits + special_chars
password += [random.choice(all_chars) for _ in range(length - 4)]
# 打乱顺序
random.shuffle(password)
return ''.join(password)
def export_to_csv(self, file_path):
"""导出密码到CSV文件"""
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('''SELECT service, username, url, notes, category,
created_at, updated_at FROM passwords ORDER BY service''')
results = c.fetchall()
conn.close()
with open(file_path, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['服务名', '用户名', '密码', 'URL', '备注', '分类', '创建时间', '更新时间'])
for row in results:
# 解密密码
password = self.get_password(row[0], row[1])
writer.writerow([row[0], row[1], password, row[2], row[3], row[4], row[5], row[6]])
return True, f"成功导出 {len(results)} 条记录到 {file_path}"
except Exception as e:
return False, f"导出失败: {str(e)}"
def import_from_csv(self, file_path):
"""从CSV文件导入密码"""
try:
imported_count = 0
skipped_count = 0
with open(file_path, 'r', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
service = row.get('服务名', '').strip()
username = row.get('用户名', '').strip()
password = row.get('密码', '').strip()
url = row.get('URL', '').strip()
notes = row.get('备注', '').strip()
category = row.get('分类', '默认').strip()
if not service or not username or not password:
skipped_count += 1
continue
# 检查是否已存在
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT id FROM passwords WHERE service = ? AND username = ?',
(service, username))
if c.fetchone():
skipped_count += 1
conn.close()
continue
# 加密密码并添加
encrypted_password = self.encrypt_password(password)
if encrypted_password:
c.execute('''INSERT INTO passwords
(service, username, encrypted_password, url, notes, category)
VALUES (?, ?, ?, ?, ?, ?)''',
(service, username, encrypted_password, url, notes, category))
conn.commit()
imported_count += 1
else:
skipped_count += 1
conn.close()
return True, f"成功导入 {imported_count} 条记录,跳过 {skipped_count} 条记录"
except Exception as e:
return False, f"导入失败: {str(e)}"
# 加密文本管理方法
def encrypt_text(self, text):
"""加密文本内容"""
try:
# 生成随机IV
iv = os.urandom(12)
# 创建加密器
cipher = Cipher(algorithms.AES(self.key), modes.GCM(iv))
encryptor = cipher.encryptor()
# 加密数据
ciphertext = encryptor.update(text.encode('utf-8')) + encryptor.finalize()
# 返回IV + 标签 + 密文 (都进行base64编码以便存储)
return base64.b64encode(iv + encryptor.tag + ciphertext).decode()
except Exception as e:
messagebox.showerror('错误', f'文本加密失败: {str(e)}')
return None
def decrypt_text(self, encrypted_text):
"""解密文本内容"""
try:
# 解码base64数据
data = base64.b64decode(encrypted_text.encode())
# 分离IV、标签和密文
iv = data[:12]
tag = data[12:28]
ciphertext = data[28:]
# 创建解密器
cipher = Cipher(algorithms.AES(self.key), modes.GCM(iv, tag))
decryptor = cipher.decryptor()
# 解密数据
return (decryptor.update(ciphertext) + decryptor.finalize()).decode('utf-8')
except Exception as e:
messagebox.showerror('错误', f'文本解密失败: {str(e)}')
return None
def add_encrypted_text(self, title, content):
"""添加加密文本"""
if not all([title.strip(), content.strip()]):
messagebox.showerror('错误', '标题和内容不能为空!')
return False
encrypted_content = self.encrypt_text(content)
if not encrypted_content:
return False
try:
from datetime import datetime
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('INSERT INTO encrypted_texts (title, encrypted_content, created_at, updated_at) VALUES (?, ?, ?, ?)',
(title.strip(), encrypted_content, current_time, current_time))
conn.commit()
conn.close()
messagebox.showinfo('成功', '加密文本添加成功!')
return True
except Exception as e:
messagebox.showerror('错误', f'添加加密文本失败: {str(e)}')
return False
def get_encrypted_text(self, text_id):
"""获取解密后的文本内容"""
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT title, encrypted_content FROM encrypted_texts WHERE id = ?', (text_id,))
result = c.fetchone()
conn.close()
if result:
title, encrypted_content = result
decrypted_content = self.decrypt_text(encrypted_content)
return title, decrypted_content
return None, None
except Exception as e:
messagebox.showerror('错误', f'获取加密文本失败: {str(e)}')
return None, None
def get_all_encrypted_texts(self, sort_by='title', sort_order='ASC'):
"""获取所有加密文本列表(不包含内容)"""
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
# 构建排序查询
valid_columns = ['id', 'title', 'created_at', 'updated_at']
if sort_by not in valid_columns:
sort_by = 'title'
if sort_order.upper() not in ['ASC', 'DESC']:
sort_order = 'ASC'
query = f'SELECT id, title, created_at, updated_at FROM encrypted_texts ORDER BY {sort_by} {sort_order}'
c.execute(query)
results = c.fetchall()
conn.close()
# 确保时间格式正确,处理可能的空值或格式问题
formatted_results = []
for result in results:
text_id, title, created_at, updated_at = result
# 处理标题
if not title:
title = "无标题"
# 处理时间格式 - 确保时间正确显示
if not created_at or str(created_at).strip() == '':
from datetime import datetime
created_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
else:
# 确保时间格式正确
created_at = str(created_at)
if not updated_at or str(updated_at).strip() == '':
from datetime import datetime
updated_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
else:
# 确保时间格式正确
updated_at = str(updated_at)
formatted_results.append((text_id, title, created_at, updated_at))
return formatted_results
except Exception as e:
messagebox.showerror('错误', f'获取加密文本列表失败: {str(e)}')
return []
def update_encrypted_text(self, text_id, title, content):
"""更新加密文本"""
if not all([title.strip(), content.strip()]):
messagebox.showerror('错误', '标题和内容不能为空!')
return False
encrypted_content = self.encrypt_text(content)
if not encrypted_content:
return False
try:
from datetime import datetime
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('UPDATE encrypted_texts SET title = ?, encrypted_content = ?, updated_at = ? WHERE id = ?',
(title.strip(), encrypted_content, current_time, text_id))
conn.commit()
affected = c.rowcount
conn.close()
if affected > 0:
messagebox.showinfo('成功', '加密文本更新成功!')
return True
else:
messagebox.showerror('错误', '未找到文本记录!')
return False
except Exception as e:
messagebox.showerror('错误', f'更新加密文本失败: {str(e)}')
return False
def delete_encrypted_text(self, text_id):
"""删除加密文本"""
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('DELETE FROM encrypted_texts WHERE id = ?', (text_id,))
conn.commit()
affected = c.rowcount
conn.close()
if affected > 0:
messagebox.showinfo('成功', '加密文本删除成功!')
return True
else:
messagebox.showerror('错误', '未找到文本记录!')
return False
except Exception as e:
messagebox.showerror('错误', f'删除加密文本失败: {str(e)}')
return False
def search_encrypted_texts(self, keyword):
"""搜索加密文本(仅搜索标题)"""
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
query = "SELECT id, title, created_at, updated_at FROM encrypted_texts WHERE title LIKE ? ORDER BY title"
c.execute(query, (f'%{keyword}%',))
results = c.fetchall()
conn.close()
# 确保时间格式正确,处理可能的空值或格式问题
formatted_results = []
for result in results:
text_id, title, created_at, updated_at = result
# 处理标题
if not title:
title = "无标题"
# 处理时间格式
if not created_at or str(created_at).strip() == '':
from datetime import datetime
created_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
if not updated_at or str(updated_at).strip() == '':
from datetime import datetime
updated_at = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
formatted_results.append((text_id, title, created_at, updated_at))
return formatted_results
except Exception as e:
messagebox.showerror('错误', f'搜索加密文本失败: {str(e)}')
return []
def export_texts_to_csv(self, file_path):
"""导出加密文本到CSV文件"""
try:
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT id, title, encrypted_content, created_at, updated_at FROM encrypted_texts ORDER BY title')
results = c.fetchall()
conn.close()
with open(file_path, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['ID', '标题', '内容', '创建时间', '更新时间'])
for row in results:
text_id, title, encrypted_content, created_at, updated_at = row
# 解密文本内容
decrypted_content = self.decrypt_text(encrypted_content)
if not decrypted_content:
decrypted_content = "[解密失败]"
writer.writerow([text_id, title, decrypted_content, created_at, updated_at])
return True, f"成功导出 {len(results)} 条文本记录到 {file_path}"
except Exception as e:
return False, f"导出失败: {str(e)}"
def import_texts_from_csv(self, file_path):
"""从CSV文件导入加密文本"""
try:
from datetime import datetime
imported_count = 0
skipped_count = 0
with open(file_path, 'r', encoding='utf-8') as csvfile:
reader = csv.reader(csvfile)
# 跳过标题行
next(reader, None)
for row in reader:
if len(row) < 3: # 至少需要ID、标题、内容
continue
# 解析CSV行:ID, 标题, 内容, 创建时间, 更新时间
text_id = row[0] if row[0] else None
title = row[1].strip() if row[1] else None
content = row[2] if row[2] else None
if not title or not content:
skipped_count += 1
continue
# 检查是否已存在相同标题的文本
conn = sqlite3.connect(self.db_path)
c = conn.cursor()
c.execute('SELECT id FROM encrypted_texts WHERE title = ?', (title,))
if c.fetchone():
skipped_count += 1
conn.close()
continue
# 加密并添加文本
encrypted_content = self.encrypt_text(content)
if encrypted_content:
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
c.execute('INSERT INTO encrypted_texts (title, encrypted_content, created_at, updated_at) VALUES (?, ?, ?, ?)',
(title, encrypted_content, current_time, current_time))
conn.commit()
imported_count += 1
else:
skipped_count += 1
conn.close()
return True, f"成功导入 {imported_count} 条文本记录,跳过 {skipped_count} 条记录"
except Exception as e:
return False, f"导入失败: {str(e)}"
class PasswordManagerGUI:
def __init__(self, root):
self.root = root
self.root.title('安全密码管理器 v1.0 by:危阿杰 吾爱破解: www.52pojie.cn')
self.root.geometry('1200x700')
self.root.resizable(True, True)
self.root.configure(bg='#f8f9fa')
# 设置现代化样式
self.style = ttk.Style()
self.style.theme_use('clam')
self.style.configure('TLabel', font=('Microsoft YaHei UI', 10), background='#f8f9fa')
self.style.configure('TButton', font=('Microsoft YaHei UI', 10), padding=6)
self.style.configure('Treeview', font=('Microsoft YaHei UI', 9), rowheight=25)
self.style.configure('Treeview.Heading', font=('Microsoft YaHei UI', 10, 'bold'))
self.style.configure('Title.TLabel', font=('Microsoft YaHei UI', 18, 'bold'), background='#f8f9fa', foreground='#2c3e50')
# 创建密码管理器实例
self.password_manager = PasswordManager()
# 悬浮窗相关属性
self.floating_window = None
self.is_floating = False
self.floating_position = {'x': 100, 'y': 100} # 默认悬浮窗位置
self.floating_expanded = False # 悬浮窗是否展开
self.floating_current_tab = 'passwords' # 当前标签页
self.floating_data = [] # 当前显示的数据
# 检查密码管理器是否成功初始化
if not hasattr(self.password_manager, 'key') or not self.password_manager.key:
# 密码管理器未完全初始化(可能是密钥文件丢失),隐藏主窗口
self.root.withdraw()
# 等待用户处理密钥文件问题
self.wait_for_key_recovery()
return
# 创建界面组件
self.create_widgets()
# 加载密码列表
self.update_password_list()
# 启动自动锁定监控
self.password_manager.auto_lock_manager.start_monitoring(self.lock_application)
# 绑定活动事件
self.bind_activity_events()
# 绑定快捷键
self.bind_shortcuts()
# 加载悬浮窗位置
self.load_floating_position()
def wait_for_key_recovery(self):
"""等待密钥恢复完成"""
def check_recovery():
if hasattr(self.password_manager, 'key') and self.password_manager.key:
# 密钥恢复成功,显示主窗口并初始化界面
self.root.deiconify()
self.create_widgets()
self.update_password_list()
self.password_manager.auto_lock_manager.start_monitoring(self.lock_application)
self.bind_activity_events()
self.bind_shortcuts()
else:
# 继续等待,每500ms检查一次
self.root.after(500, check_recovery)
# 开始检查
check_recovery()
def create_menu(self):
"""创建菜单栏"""
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
# 获取快捷键管理器
sm = self.password_manager.shortcut_manager
# 文件菜单
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="文件", menu=file_menu)
file_menu.add_command(label=f"📥 导入CSV\t{sm.get_shortcut('import_csv')}", command=self.import_csv)
file_menu.add_command(label=f"📤 导出CSV\t{sm.get_shortcut('export_csv')}", command=self.export_csv)
file_menu.add_separator()
file_menu.add_command(label="📥 导入文本CSV", command=self.import_texts_menu)
file_menu.add_command(label="📤 导出文本CSV", command=self.export_texts_menu)
file_menu.add_separator()
file_menu.add_command(label="💾 备份密钥文件", command=self.backup_key_file)
file_menu.add_command(label="🔄 恢复密钥文件", command=self.restore_key_file)
file_menu.add_separator()
file_menu.add_command(label=f"🔒 锁定应用\t{sm.get_shortcut('lock_app')}", command=self.lock_application)
file_menu.add_separator()
file_menu.add_command(label="❌ 退出", command=self.root.quit)
# 编辑菜单
edit_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="编辑", menu=edit_menu)
edit_menu.add_command(label=f"➕ 添加密码\t{sm.get_shortcut('add_password')}", command=self.add_password_dialog)
edit_menu.add_command(label=f"👁️ 查看密码\t{sm.get_shortcut('view_password')}", command=self.view_password)
edit_menu.add_command(label=f"✏️ 编辑密码\t{sm.get_shortcut('edit_password')}", command=self.update_password_dialog)
edit_menu.add_command(label=f"🗑️ 删除密码\t{sm.get_shortcut('delete_password')}", command=self.delete_password)
edit_menu.add_separator()
edit_menu.add_command(label=f"🎲 生成密码\t{sm.get_shortcut('generate_password')}", command=self.generate_password)
edit_menu.add_command(label=f"🔍 搜索\t{sm.get_shortcut('search')}", command=lambda: self.search_entry.focus())
# 工具菜单
tools_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="工具", menu=tools_menu)
tools_menu.add_command(label="🔍 密码强度检查", command=self.check_all_passwords_strength)
tools_menu.add_command(label="⏰ 过期密码检查", command=self.check_expired_passwords)
tools_menu.add_separator()
tools_menu.add_command(label="📝 文本管理\tF2", command=self.show_text_manager)
tools_menu.add_separator()
tools_menu.add_command(label="🏷️ 管理分类", command=self.manage_categories)
tools_menu.add_separator()
tools_menu.add_command(label="⌨️ 快捷键设置", command=self.show_shortcut_settings)
# 视图菜单
view_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="视图", menu=view_menu)
# 悬浮窗选项
view_menu.add_command(label=f"🪟 悬浮窗模式\t{sm.get_shortcut('toggle_floating')}", command=self.toggle_floating_mode)
view_menu.add_separator()
# 主题子菜单
theme_menu = tk.Menu(view_menu, tearoff=0)
view_menu.add_cascade(label="🎨 主题", menu=theme_menu)
theme_menu.add_command(label="☀️ 浅色主题", command=lambda: self.change_theme('light'))
theme_menu.add_command(label="🌙 深色主题", command=lambda: self.change_theme('dark'))
theme_menu.add_command(label="🔵 蓝色主题", command=lambda: self.change_theme('blue'))
theme_menu.add_separator()
theme_menu.add_command(label="🌿 绿色主题", command=lambda: self.change_theme('green'))
theme_menu.add_command(label="💜 紫色主题", command=lambda: self.change_theme('purple'))
theme_menu.add_command(label="🧡 橙色主题", command=lambda: self.change_theme('orange'))
theme_menu.add_separator()
theme_menu.add_command(label="⚫ 高对比度", command=lambda: self.change_theme('high_contrast'))
theme_menu.add_command(label="🌃 夜间模式", command=lambda: self.change_theme('night'))
# 帮助菜单
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="帮助", menu=help_menu)
help_menu.add_command(label="📖 使用说明", command=self.show_help)
help_menu.add_command(label="🔑 密钥恢复指南", command=self.show_key_recovery_guide)
help_menu.add_separator()
help_menu.add_command(label="ℹ️ 关于", command=self.show_about)
def create_widgets(self):
# 创建菜单栏
self.create_menu()
# 创建主框架
main_frame = ttk.Frame(self.root, padding='15')
main_frame.pack(fill=tk.BOTH, expand=True)
# 创建标题
title_label = ttk.Label(main_frame, text='🔐 安全密码管理器', style='Title.TLabel')
title_label.pack(pady=(0, 20))
# 创建按钮框架
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, pady=(0, 15))
# 添加按钮 - 使用更现代的图标和文字
ttk.Button(button_frame, text='➕ 添加密码', command=self.add_password_dialog, width=18).pack(side=tk.LEFT, padx=8)
ttk.Button(button_frame, text='👁️ 查看密码', command=self.view_password, width=18).pack(side=tk.LEFT, padx=8)
ttk.Button(button_frame, text='✏️ 更新密码', command=self.update_password_dialog, width=18).pack(side=tk.LEFT, padx=8)
ttk.Button(button_frame, text='🗑️ 删除密码', command=self.delete_password, width=18).pack(side=tk.LEFT, padx=8)
ttk.Button(button_frame, text='🎲 生成密码', command=self.generate_password, width=18).pack(side=tk.LEFT, padx=8)
# 创建搜索框
search_frame = ttk.Frame(main_frame)
search_frame.pack(fill=tk.X, pady=(0, 15))
ttk.Label(search_frame, text='🔍 搜索:', font=('Microsoft YaHei UI', 11)).pack(side=tk.LEFT, padx=(0, 10))
self.search_var = tk.StringVar()
self.search_entry = ttk.Entry(search_frame, textvariable=self.search_var, font=('Microsoft YaHei UI', 10), width=40)
self.search_entry.pack(side=tk.LEFT, padx=(0, 10))
self.search_entry.bind('<Return>', lambda event: self.search_passwords())
ttk.Button(search_frame, text='🔍 搜索', command=self.search_passwords, width=12).pack(side=tk.LEFT, padx=5)
ttk.Button(search_frame, text='🔄 重置', command=self.reset_search, width=12).pack(side=tk.LEFT, padx=5)
# 创建密码列表
columns = ('ID', '名称', '用户名', 'URL', '备注', '创建时间', '更新时间')
self.password_tree = ttk.Treeview(main_frame, columns=columns, show='headings', selectmode='browse')
# 排序状态
self.sort_reverse = {}
for col in columns:
self.password_tree.heading(col, text=col, command=lambda c=col: self.sort_column(c))
if col == 'ID':
self.password_tree.column(col, width=50, anchor=tk.CENTER)
elif col in ['创建时间', '更新时间']:
self.password_tree.column(col, width=150, anchor=tk.CENTER)
elif col == 'URL':
self.password_tree.column(col, width=200, anchor=tk.W)
self.password_tree.tag_configure('url', foreground='blue')
elif col == '备注':
self.password_tree.column(col, width=150, anchor=tk.W)
elif col == '名称':
self.password_tree.column(col, width=150, anchor=tk.CENTER)
else:
self.password_tree.column(col, width=120, anchor=tk.CENTER)
# 初始化排序状态
self.sort_reverse[col] = False
# 添加滚动条
scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=self.password_tree.yview)
self.password_tree.configure(yscroll=scrollbar.set)
self.password_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, pady=5)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 创建状态栏框架
status_frame = ttk.Frame(self.root)
status_frame.pack(side=tk.BOTTOM, fill=tk.X)
# 主状态标签
self.status_var = tk.StringVar()
self.status_var.set('就绪')
status_label = ttk.Label(status_frame, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W)
status_label.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=(2, 5), pady=2)
# 时间标签
self.time_var = tk.StringVar()
time_label = ttk.Label(status_frame, textvariable=self.time_var, relief=tk.SUNKEN, anchor=tk.CENTER, width=20)
time_label.pack(side=tk.RIGHT, padx=(5, 2), pady=2)
# 更新时间
self.update_time()
# 绑定双击事件查看密码或打开链接
self.password_tree.bind('<Double-1>', self.on_double_click)
# 绑定右键菜单
self.password_tree.bind('<Button-3>', self.show_context_menu)
def on_double_click(self, event):
# 获取双击的项和列
if not self.password_tree.selection():
return
item = self.password_tree.selection()[0]
column = self.password_tree.identify_column(event.x)
# 列索引从1开始,URL是第4列(索引3)
if column == '#4': # '#4'对应第4列,即URL列
url = self.password_tree.item(item, 'values')[3]
if url:
webbrowser.open(url)
else:
messagebox.showinfo('提示', '该记录没有URL链接')
else:
# 双击其他列时查看密码详情
self.view_password()
def update_password_list(self, passwords=None):
# 更新密码列表
# 清空现有项
for item in self.password_tree.get_children():
self.password_tree.delete(item)
# 获取密码列表
if passwords is None:
passwords = self.password_manager.get_all_passwords()
# 添加到列表
for password in passwords:
item_id = self.password_tree.insert('', tk.END, values=password)
# 如果URL不为空,设置URL标签
if password[3]: # password[3]是URL字段
self.password_tree.item(item_id, tags=('url',))
self.status_var.set(f'共 {len(passwords)} 条记录')
def add_password_dialog(self):
# 添加密码对话框
dialog = tk.Toplevel(self.root)
dialog.title('添加新密码')
dialog.geometry('550x350')
dialog.resizable(False, False)
dialog.transient(self.root)
dialog.grab_set()
# 居中显示
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
# 创建框架
frame = ttk.Frame(dialog, padding='25')
frame.pack(fill=tk.BOTH, expand=True)
# 配置列权重
frame.grid_columnconfigure(1, weight=1)
# 服务名
ttk.Label(frame, text='名称:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=0, column=0, sticky=tk.W, pady=10, padx=(0, 15))
service_var = tk.StringVar()
service_entry = ttk.Entry(frame, textvariable=service_var, width=30, font=('Microsoft YaHei UI', 10))
service_entry.grid(row=0, column=1, columnspan=2, sticky=tk.W+tk.E, pady=10)
# 用户名
ttk.Label(frame, text='用户名:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=1, column=0, sticky=tk.W, pady=10, padx=(0, 15))
username_var = tk.StringVar()
username_entry = ttk.Entry(frame, textvariable=username_var, width=30, font=('Microsoft YaHei UI', 10))
username_entry.grid(row=1, column=1, columnspan=2, sticky=tk.W+tk.E, pady=10)
# 密码行
ttk.Label(frame, text='密码:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=2, column=0, sticky=tk.W, pady=10, padx=(0, 15))
# 密码输入框架
password_input_frame = ttk.Frame(frame)
password_input_frame.grid(row=2, column=1, columnspan=2, sticky=tk.W+tk.E, pady=10)
password_input_frame.grid_columnconfigure(0, weight=1)
password_var = tk.StringVar()
password_entry = ttk.Entry(password_input_frame, textvariable=password_var, font=('Consolas', 10))
password_entry.grid(row=0, column=0, sticky=tk.W+tk.E, padx=(0, 10))
# 生成密码按钮
def generate_new_password():
new_password = self.password_manager.generate_strong_password()
password_var.set(new_password)
update_strength()
ttk.Button(password_input_frame, text='生成强密码', command=generate_new_password, width=12).grid(row=0, column=1)
# 密码强度显示
strength_frame = ttk.Frame(frame)
strength_frame.grid(row=3, column=1, columnspan=2, sticky=tk.W+tk.E, pady=(5, 10))
strength_frame.grid_columnconfigure(1, weight=1)
strength_label = ttk.Label(strength_frame, text='强度: 未检测', font=('Microsoft YaHei UI', 9))
strength_label.grid(row=0, column=0, sticky=tk.W)
strength_bar = ttk.Progressbar(strength_frame, length=250, mode='determinate')
strength_bar.grid(row=0, column=1, sticky=tk.W+tk.E, padx=(10, 0))
def update_strength(*args):
password = password_var.get()
if password:
score, level, color, feedback = self.password_manager.strength_checker.check_strength(password)
strength_label.config(text=f'强度: {level}', foreground=color)
strength_bar['value'] = score
if feedback:
# 显示最重要的反馈
main_feedback = feedback[0] if feedback else ""
strength_label.config(text=f'强度: {level} - {main_feedback}')
else:
strength_label.config(text='强度: 未检测', foreground='gray')
strength_bar['value'] = 0
password_var.trace('w', update_strength)
# URL
ttk.Label(frame, text='URL:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=4, column=0, sticky=tk.W, pady=10, padx=(0, 15))
url_var = tk.StringVar()
url_entry = ttk.Entry(frame, textvariable=url_var, font=('Microsoft YaHei UI', 10))
url_entry.grid(row=4, column=1, columnspan=2, sticky=tk.W+tk.E, pady=10)
# 备注
ttk.Label(frame, text='备注:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=5, column=0, sticky=tk.W, pady=10, padx=(0, 15))
notes_var = tk.StringVar()
notes_entry = ttk.Entry(frame, textvariable=notes_var, font=('Microsoft YaHei UI', 10))
notes_entry.grid(row=5, column=1, columnspan=2, sticky=tk.W+tk.E, pady=10)
# 按钮框架
btn_frame = ttk.Frame(frame)
btn_frame.grid(row=6, column=0, columnspan=3, pady=15)
def save_password():
service = service_var.get().strip()
username = username_var.get().strip()
password = password_var.get().strip()
url = url_var.get().strip()
notes = notes_var.get().strip()
if self.password_manager.add_password(service, username, password, url, notes):
self.update_password_list()
dialog.destroy()
ttk.Button(btn_frame, text='保存', command=save_password).pack(side=tk.LEFT, padx=10)
ttk.Button(btn_frame, text='取消', command=dialog.destroy).pack(side=tk.LEFT, padx=10)
def view_password(self):
# 查看密码
selected_item = self.password_tree.selection()
if not selected_item:
messagebox.showwarning('提示', '请先选择一条记录!')
return
item = self.password_tree.item(selected_item[0])
password_id, service, username = item['values'][0], item['values'][1], item['values'][2]
# 获取解密后的密码
password = self.password_manager.get_password(service, username)
if not password:
messagebox.showerror('错误', '无法获取密码!')
return
# 显示密码对话框
dialog = tk.Toplevel(self.root)
dialog.title(f'👁️ {service} - 密码详情')
dialog.geometry('650x400')
dialog.resizable(True, True)
dialog.transient(self.root)
dialog.grab_set()
dialog.configure(bg='#f8f9fa')
# 居中显示
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
# 创建框架
frame = ttk.Frame(dialog, padding='25')
frame.pack(fill=tk.BOTH, expand=True)
# 标题
title_label = ttk.Label(frame, text=f'🔐 {service}', font=('Microsoft YaHei UI', 14, 'bold'), foreground='#2c3e50')
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
# 服务名
ttk.Label(frame, text='名称:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=1, column=0, sticky=tk.W, pady=10, padx=(0, 15))
service_label = ttk.Label(frame, text=service, font=('Microsoft YaHei UI', 12), foreground='#34495e')
service_label.grid(row=1, column=1, sticky=tk.W, pady=10, columnspan=2)
# 用户名
ttk.Label(frame, text='用户名:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=2, column=0, sticky=tk.W, pady=10, padx=(0, 15))
username_label = ttk.Label(frame, text=username, font=('Microsoft YaHei UI', 12), foreground='#34495e')
username_label.grid(row=2, column=1, sticky=tk.W, pady=10, columnspan=2)
# 密码
ttk.Label(frame, text='密码:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=3, column=0, sticky=tk.W, pady=12, padx=(0, 15))
# 密码显示框架
password_frame = ttk.Frame(frame)
password_frame.grid(row=3, column=1, sticky=tk.W+tk.E, pady=12, columnspan=2)
password_frame.grid_columnconfigure(0, weight=1)
# 密码显示框 - 使用Entry组件以便复制
password_display = tk.Entry(password_frame,
font=('Consolas', 12, 'bold'),
fg='#e74c3c',
bg='#ffffff',
relief='solid',
bd=1,
state='readonly',
readonlybackground='#ffffff')
password_display.insert(0, password)
password_display.pack(side=tk.LEFT, padx=(0, 15), fill=tk.X, expand=True)
# 复制按钮
def copy_to_clipboard():
pyperclip.copy(password)
messagebox.showinfo('✅ 成功', '密码已复制到剪贴板!')
ttk.Button(password_frame, text='📋 复制', command=copy_to_clipboard, width=12).pack(side=tk.RIGHT)
current_row = 4
# URL
if item['values'][3]:
ttk.Label(frame, text='URL:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=current_row, column=0, sticky=tk.W, pady=8, padx=(0, 15))
url_text = item['values'][3]
url_label = ttk.Label(frame, text=url_text, font=('Microsoft YaHei UI', 10), foreground='#3498db', cursor='hand2')
url_label.grid(row=current_row, column=1, sticky=tk.W, pady=8, columnspan=2)
# 点击URL打开链接
def open_url(event):
webbrowser.open(url_text)
url_label.bind('<Button-1>', open_url)
current_row += 1
# 备注
if item['values'][4]:
ttk.Label(frame, text='备注:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=current_row, column=0, sticky=tk.W, pady=8, padx=(0, 15))
notes_label = ttk.Label(frame, text=item['values'][4], font=('Microsoft YaHei UI', 10), foreground='#34495e', wraplength=300)
notes_label.grid(row=current_row, column=1, sticky=tk.W, pady=8, columnspan=2)
current_row += 1
# 时间信息
ttk.Label(frame, text='创建时间:', font=('Microsoft YaHei UI', 9)).grid(row=current_row, column=0, sticky=tk.W, pady=(15, 5), padx=(0, 15))
ttk.Label(frame, text=item['values'][5], font=('Microsoft YaHei UI', 9), foreground='#7f8c8d').grid(row=current_row, column=1, sticky=tk.W, pady=(15, 5))
ttk.Label(frame, text='更新时间:', font=('Microsoft YaHei UI', 9)).grid(row=current_row+1, column=0, sticky=tk.W, pady=5, padx=(0, 15))
ttk.Label(frame, text=item['values'][6], font=('Microsoft YaHei UI', 9), foreground='#7f8c8d').grid(row=current_row+1, column=1, sticky=tk.W, pady=5)
# 按钮框架
btn_frame = ttk.Frame(frame)
btn_frame.grid(row=current_row+2, column=0, columnspan=3, pady=(20, 0))
# 关闭按钮
ttk.Button(btn_frame, text='❌ 关闭', command=dialog.destroy, width=15).pack(side=tk.RIGHT, padx=10)
# 编辑按钮
def edit_password():
dialog.destroy()
self.update_password_dialog()
ttk.Button(btn_frame, text='✏️ 编辑', command=edit_password, width=15).pack(side=tk.RIGHT, padx=10)
def update_password_dialog(self):
# 更新密码对话框
selected_item = self.password_tree.selection()
if not selected_item:
messagebox.showwarning('提示', '请先选择一条记录!')
return
item = self.password_tree.item(selected_item[0])
password_id, service, username = item['values'][0], item['values'][1], item['values'][2]
# 显示更新对话框
dialog = tk.Toplevel(self.root)
dialog.title(f'{service} - 更新密码')
dialog.geometry('550x320')
dialog.resizable(False, False)
dialog.transient(self.root)
dialog.grab_set()
# 居中显示
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
# 创建框架
frame = ttk.Frame(dialog, padding='25')
frame.pack(fill=tk.BOTH, expand=True)
# 配置列权重
frame.grid_columnconfigure(1, weight=1)
# 服务名称
ttk.Label(frame, text='名称:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=0, column=0, sticky=tk.W, pady=10, padx=(0, 15))
service_var = tk.StringVar(value=service)
service_entry = ttk.Entry(frame, textvariable=service_var, font=('Microsoft YaHei UI', 10))
service_entry.grid(row=0, column=1, columnspan=2, sticky=tk.W+tk.E, pady=10)
# 用户名
ttk.Label(frame, text='用户名:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=1, column=0, sticky=tk.W, pady=10, padx=(0, 15))
username_var = tk.StringVar(value=username)
username_entry = ttk.Entry(frame, textvariable=username_var, font=('Microsoft YaHei UI', 10))
username_entry.grid(row=1, column=1, columnspan=2, sticky=tk.W+tk.E, pady=10)
# 密码行
ttk.Label(frame, text='新密码:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=2, column=0, sticky=tk.W, pady=10, padx=(0, 15))
# 密码输入框架
password_input_frame = ttk.Frame(frame)
password_input_frame.grid(row=2, column=1, columnspan=2, sticky=tk.W+tk.E, pady=10)
password_input_frame.grid_columnconfigure(0, weight=1)
password_var = tk.StringVar()
password_entry = ttk.Entry(password_input_frame, textvariable=password_var, font=('Consolas', 10))
password_entry.grid(row=0, column=0, sticky=tk.W+tk.E, padx=(0, 10))
# 生成密码按钮
def generate_new_password():
new_password = self.password_manager.generate_strong_password()
password_var.set(new_password)
ttk.Button(password_input_frame, text='生成强密码', command=generate_new_password, width=12).grid(row=0, column=1)
# URL
ttk.Label(frame, text='URL:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=3, column=0, sticky=tk.W, pady=10, padx=(0, 15))
url_var = tk.StringVar(value=item['values'][3])
url_entry = ttk.Entry(frame, textvariable=url_var, font=('Microsoft YaHei UI', 10))
url_entry.grid(row=3, column=1, columnspan=2, sticky=tk.W+tk.E, pady=10)
# 备注
ttk.Label(frame, text='备注:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=4, column=0, sticky=tk.W, pady=10, padx=(0, 15))
notes_var = tk.StringVar(value=item['values'][4])
notes_entry = ttk.Entry(frame, textvariable=notes_var, font=('Microsoft YaHei UI', 10))
notes_entry.grid(row=4, column=1, columnspan=2, sticky=tk.W+tk.E, pady=10)
# 按钮框架
btn_frame = ttk.Frame(frame)
btn_frame.grid(row=5, column=0, columnspan=3, pady=25)
def save_update():
service = service_var.get().strip()
username = username_var.get().strip()
new_password = password_var.get().strip()
url = url_var.get().strip()
notes = notes_var.get().strip()
# 检查必填字段
if not service or not username:
messagebox.showerror('错误', '服务名和用户名不能为空!')
return
# 如果没有输入新密码,提示用户
if not new_password:
confirm = messagebox.askyesno('确认', '您没有输入新密码,是否只更新其他信息?')
if not confirm:
return
success = self.password_manager.update_password(password_id, service, username, new_password, url, notes)
if success:
self.update_password_list()
dialog.destroy()
# update_password 方法内部已经显示了成功或错误消息
ttk.Button(btn_frame, text='保存', command=save_update).pack(side=tk.LEFT, padx=10)
ttk.Button(btn_frame, text='取消', command=dialog.destroy).pack(side=tk.LEFT, padx=10)
def delete_password(self):
# 删除密码
selected_item = self.password_tree.selection()
if not selected_item:
messagebox.showwarning('提示', '请先选择一条记录!')
return
item = self.password_tree.item(selected_item[0])
password_id, service, username = item['values'][0], item['values'][1], item['values'][2]
confirm = messagebox.askyesno('确认', f'确定要删除 {service} 的 {username} 密码记录吗?')
if confirm:
if self.password_manager.delete_password(password_id):
self.update_password_list()
def search_passwords(self):
# 搜索密码
keyword = self.search_var.get().strip()
if not keyword:
self.update_password_list()
return
results = self.password_manager.search_passwords(keyword)
self.update_password_list(results)
self.status_var.set(f'搜索到 {len(results)} 条记录')
def reset_search(self):
# 重置搜索
self.search_var.set('')
self.update_password_list()
def generate_password(self):
# 生成密码对话框
password = self.password_manager.generate_strong_password()
dialog = tk.Toplevel(self.root)
dialog.title('🎲 生成强密码')
dialog.geometry('500x200')
dialog.resizable(False, False)
dialog.transient(self.root)
dialog.grab_set()
dialog.configure(bg='#f8f9fa')
# 居中显示
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
# 创建框架
frame = ttk.Frame(dialog, padding='25')
frame.pack(fill=tk.BOTH, expand=True)
# 标题
title_label = ttk.Label(frame, text='🔐 强密码生成器', font=('Microsoft YaHei UI', 14, 'bold'), foreground='#2c3e50')
title_label.grid(row=0, column=0, columnspan=3, pady=(0, 20))
# 密码显示
ttk.Label(frame, text='生成的密码:', font=('Microsoft YaHei UI', 10, 'bold')).grid(row=1, column=0, sticky=tk.W, pady=10, padx=(0, 15))
password_frame = ttk.Frame(frame)
password_frame.grid(row=1, column=1, sticky=tk.W, pady=10)
# 密码文本框
password_var = tk.StringVar(value=password)
password_entry = ttk.Entry(password_frame, textvariable=password_var, width=25,
font=('Consolas', 12, 'bold'), state='readonly')
password_entry.pack(side=tk.LEFT, padx=(0, 10))
# 复制按钮
def copy_to_clipboard():
current_password = password_var.get()
pyperclip.copy(current_password)
messagebox.showinfo('✅ 成功', '密码已复制到剪贴板!')
dialog.destroy()
# 重新生成按钮
def regenerate_password():
new_password = self.password_manager.generate_strong_password()
password_var.set(new_password)
# 按钮框架
btn_frame = ttk.Frame(frame)
btn_frame.grid(row=2, column=0, columnspan=3, pady=(20, 0))
ttk.Button(btn_frame, text='🔄 重新生成', command=regenerate_password, width=15).pack(side=tk.LEFT, padx=10)
ttk.Button(btn_frame, text='📋 复制并关闭', command=copy_to_clipboard, width=15).pack(side=tk.LEFT, padx=10)
ttk.Button(btn_frame, text='❌ 关闭', command=dialog.destroy, width=15).pack(side=tk.LEFT, padx=10)
def bind_activity_events(self):
"""绑定用户活动事件"""
def on_activity(event=None):
self.password_manager.auto_lock_manager.update_activity()
# 绑定各种用户活动事件
self.root.bind('<Motion>', on_activity)
self.root.bind('<Button>', on_activity)
self.root.bind('<Key>', on_activity)
self.root.bind('<MouseWheel>', on_activity)
# 绑定到所有子组件
def bind_to_children(widget):
widget.bind('<Motion>', on_activity, add='+')
widget.bind('<Button>', on_activity, add='+')
widget.bind('<Key>', on_activity, add='+')
for child in widget.winfo_children():
bind_to_children(child)
bind_to_children(self.root)
def bind_shortcuts(self):
"""绑定快捷键"""
# 获取快捷键管理器并确保已初始化
sm = self.password_manager.shortcut_manager
# 确保快捷键管理器已经初始化
if sm is None:
# 如果快捷键管理器未初始化,创建一个新的
sm = ShortcutManager(self.password_manager.db_path)
self.password_manager.shortcut_manager = sm
# 绑定动态快捷键
shortcuts = {
'add_password': lambda e: self.add_password_dialog(),
'view_password': lambda e: self.view_password() if self.password_tree.selection() else None,
'edit_password': lambda e: self.update_password_dialog(),
'delete_password': lambda e: self.delete_password(),
'generate_password': lambda e: self.generate_password(),
'search': lambda e: self.search_entry.focus(),
'lock_app': lambda e: self.lock_application(),
'import_csv': lambda e: self.import_csv(),
'export_csv': lambda e: self.export_csv(),
'reset_search': lambda e: self.reset_search(),
'refresh': lambda e: self.update_password_list(),
'copy_password': self.copy_selected_password,
'toggle_floating': lambda e: self.toggle_floating_mode()
}
for action, callback in shortcuts.items():
shortcut = sm.get_shortcut(action)
if shortcut:
binding = sm.format_shortcut_binding(shortcut)
if binding:
try:
self.root.bind(binding, callback)
except tk.TclError:
pass
# 固定快捷键(不可自定义)
self.root.bind('<Escape>', lambda e: self.password_tree.selection_remove(self.password_tree.selection()))
self.root.bind('<Return>', lambda e: self.view_password() if self.password_tree.selection() else None)
self.root.bind('<F2>', lambda e: self.show_text_manager()) # F2打开文本管理器
def copy_selected_password(self, event=None):
"""复制选中项的密码"""
selected_item = self.password_tree.selection()
if selected_item:
item = self.password_tree.item(selected_item[0])
service, username = item['values'][1], item['values'][2]
password = self.password_manager.get_password(service, username)
if password:
pyperclip.copy(password)
self.status_var.set(f'已复制 {service} 的密码到剪贴板')
# 3秒后恢复状态栏
self.root.after(3000, lambda: self.status_var.set('就绪'))
else:
messagebox.showerror('错误', '无法获取密码!')
def show_context_menu(self, event):
"""显示右键菜单"""
# 选中右键点击的项
item = self.password_tree.identify_row(event.y)
if item:
self.password_tree.selection_set(item)
# 创建右键菜单
context_menu = tk.Menu(self.root, tearoff=0)
context_menu.add_command(label="👁️ 查看密码", command=self.view_password)
context_menu.add_command(label="✏️ 编辑密码", command=self.update_password_dialog)
context_menu.add_command(label="📋 复制密码", command=self.copy_selected_password)
context_menu.add_separator()
# 如果有URL,添加打开链接选项
selected_item = self.password_tree.selection()
if selected_item:
item_values = self.password_tree.item(selected_item[0])['values']
if len(item_values) > 3 and item_values[3]: # 有URL
context_menu.add_command(label="🌐 打开链接",
command=lambda: webbrowser.open(item_values[3]))
context_menu.add_separator()
context_menu.add_command(label="🗑️ 删除密码", command=self.delete_password)
# 显示菜单
try:
context_menu.tk_popup(event.x_root, event.y_root)
finally:
context_menu.grab_release()
else:
# 空白区域右键菜单
context_menu = tk.Menu(self.root, tearoff=0)
context_menu.add_command(label="➕ 添加密码", command=self.add_password_dialog)
context_menu.add_command(label="🎲 生成密码", command=self.generate_password)
context_menu.add_separator()
context_menu.add_command(label="🔄 刷新列表", command=self.update_password_list)
try:
context_menu.tk_popup(event.x_root, event.y_root)
finally:
context_menu.grab_release()
def lock_application(self):
"""锁定应用程序"""
if self.password_manager.is_locked:
return
self.password_manager.is_locked = True
# 创建锁定对话框
lock_dialog = tk.Toplevel(self.root)
lock_dialog.title('🔒 应用程序已锁定')
lock_dialog.geometry('450x250')
lock_dialog.resizable(False, False)
lock_dialog.transient(self.root)
lock_dialog.grab_set()
lock_dialog.configure(bg='#f8f9fa')
# 禁用主窗口
self.root.withdraw()
# 居中显示
lock_dialog.update_idletasks()
width = lock_dialog.winfo_width()
height = lock_dialog.winfo_height()
x = (lock_dialog.winfo_screenwidth() // 2) - (width // 2)
y = (lock_dialog.winfo_screenheight() // 2) - (height // 2)
lock_dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
frame = ttk.Frame(lock_dialog, padding='30')
frame.pack(fill=tk.BOTH, expand=True)
# 锁定图标和提示
ttk.Label(frame, text='🔒', font=('Microsoft YaHei UI', 48)).pack(pady=(0, 10))
ttk.Label(frame, text='应用程序已自动锁定', font=('Microsoft YaHei UI', 14, 'bold')).pack(pady=5)
ttk.Label(frame, text='请输入主密码解锁', font=('Microsoft YaHei UI', 10)).pack(pady=5)
# 获取解锁快捷键
unlock_shortcut = self.password_manager.shortcut_manager.get_shortcut('unlock_app')
ttk.Label(frame, text=f'或按 {unlock_shortcut} 快速解锁',
font=('Microsoft YaHei UI', 9), foreground='#666666').pack(pady=2)
# 密码输入
password_var = tk.StringVar()
password_entry = ttk.Entry(frame, textvariable=password_var, show='*', width=30, font=('Microsoft YaHei UI', 10))
password_entry.pack(pady=10)
password_entry.focus()
def unlock():
password = password_var.get()
if password and hashlib.sha256(password.encode()).hexdigest() == self.password_manager.master_password_hash:
self.password_manager.is_locked = False
self.password_manager.auto_lock_manager.update_activity()
lock_dialog.destroy()
self.root.deiconify()
else:
messagebox.showerror('错误', '密码错误!')
password_var.set('')
password_entry.focus()
password_entry.bind('<Return>', lambda e: unlock())
# 绑定解锁快捷键
def handle_unlock_shortcut(event):
"""处理解锁快捷键"""
unlock()
# 绑定快捷键到对话框
unlock_shortcut = self.password_manager.shortcut_manager.get_shortcut('unlock_app')
if unlock_shortcut:
try:
lock_dialog.bind(f'<{unlock_shortcut}>', handle_unlock_shortcut)
# 确保对话框能接收键盘事件
lock_dialog.focus_set()
except tk.TclError:
pass # 如果快捷键格式不正确,忽略错误
btn_frame = ttk.Frame(frame)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text='🔓 解锁', command=unlock, width=15).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text='❌ 退出', command=self.root.quit, width=15).pack(side=tk.LEFT, padx=5)
def export_csv(self):
"""导出CSV对话框"""
file_path = filedialog.asksaveasfilename(
title="导出密码数据",
defaultextension=".csv",
filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")]
)
if file_path:
success, message = self.password_manager.export_to_csv(file_path)
if success:
messagebox.showinfo("导出成功", message)
else:
messagebox.showerror("导出失败", message)
def import_csv(self):
"""导入CSV对话框"""
file_path = filedialog.askopenfilename(
title="导入密码数据",
filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")]
)
if file_path:
confirm = messagebox.askyesno("确认导入",
"导入操作将添加新的密码记录。\n已存在的记录将被跳过。\n是否继续?")
if confirm:
success, message = self.password_manager.import_from_csv(file_path)
if success:
messagebox.showinfo("导入完成", message)
self.update_password_list()
else:
messagebox.showerror("导入失败", message)
def check_all_passwords_strength(self):
"""检查所有密码强度"""
passwords = self.password_manager.get_all_passwords()
weak_passwords = []
for password_info in passwords:
service, username = password_info[1], password_info[2]
password = self.password_manager.get_password(service, username)
if password:
score, level, color, feedback = self.password_manager.strength_checker.check_strength(password)
if score < 60: # 强度低于60分的密码
weak_passwords.append((service, username, level, score))
if weak_passwords:
# 显示弱密码报告
report_dialog = tk.Toplevel(self.root)
report_dialog.title('🔍 密码强度报告')
report_dialog.geometry('600x400')
report_dialog.transient(self.root)
report_dialog.grab_set()
frame = ttk.Frame(report_dialog, padding='20')
frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(frame, text=f'发现 {len(weak_passwords)} 个弱密码',
font=('Microsoft YaHei UI', 14, 'bold')).pack(pady=(0, 10))
# 创建列表
columns = ('服务名', '用户名', '强度等级', '分数')
tree = ttk.Treeview(frame, columns=columns, show='headings', height=15)
for col in columns:
tree.heading(col, text=col)
tree.column(col, width=120)
for weak in weak_passwords:
tree.insert('', tk.END, values=weak)
tree.pack(fill=tk.BOTH, expand=True, pady=10)
ttk.Button(frame, text='关闭', command=report_dialog.destroy).pack(pady=10)
else:
messagebox.showinfo("密码强度检查", "所有密码强度都很好!")
def check_expired_passwords(self):
"""检查过期密码"""
messagebox.showinfo("功能开发中", "密码过期检查功能正在开发中...")
def manage_categories(self):
"""管理分类"""
messagebox.showinfo("功能开发中", "分类管理功能正在开发中...")
def show_help(self):
"""显示帮助"""
help_text = """
🔐 安全密码管理器 使用说明
主要功能:
• 添加、查看、编辑、删除密码
• 自动生成强密码
• 密码强度检测
• 数据导入导出(CSV格式)
• 自动锁定保护
快捷操作:
• 双击URL列可直接打开链接
• 应用会在15分钟无操作后自动锁定
• 支持CSV格式的数据导入导出
安全特性:
• 使用AES-GCM加密存储密码
• 配置文件使用机器特征加密
• 密码强度实时检测
• 自动锁定防止未授权访问
"""
help_dialog = tk.Toplevel(self.root)
help_dialog.title('📖 使用说明')
help_dialog.geometry('500x400')
help_dialog.transient(self.root)
frame = ttk.Frame(help_dialog, padding='20')
frame.pack(fill=tk.BOTH, expand=True)
text_widget = tk.Text(frame, wrap=tk.WORD, font=('Microsoft YaHei UI', 10))
text_widget.insert(tk.END, help_text)
text_widget.config(state=tk.DISABLED)
text_widget.pack(fill=tk.BOTH, expand=True)
ttk.Button(frame, text='关闭', command=help_dialog.destroy).pack(pady=10)
def show_about(self):
"""显示关于信息"""
about_text = """
🔐 安全密码管理器 v1.0
一个功能强大的本地密码管理工具
主要特性:
✅ 军用级AES加密
✅ 密码强度检测
✅ 自动锁定保护
✅ 数据导入导出
✅ 现代化界面设计
开发信息:
• 基于Python + Tkinter
• 使用SQLite数据库
• 采用AES-GCM加密算法
版权所有 © 2025
"""
messagebox.showinfo("关于", about_text)
def backup_key_file(self):
"""备份密钥文件"""
if not os.path.exists(self.password_manager.key_path):
messagebox.showerror("备份失败", "密钥文件不存在!")
return
file_path = filedialog.asksaveasfilename(
title="备份密钥文件",
defaultextension=".key",
filetypes=[("密钥文件", "*.key"), ("所有文件", "*.*")],
initialfile=f"secret_backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.key"
)
if file_path:
try:
import shutil
shutil.copy2(self.password_manager.key_path, file_path)
messagebox.showinfo("备份成功", f"密钥文件已备份到:\n{file_path}\n\n请妥善保管此文件!")
except Exception as e:
messagebox.showerror("备份失败", f"无法备份密钥文件:{str(e)}")
def restore_key_file(self):
"""恢复密钥文件"""
confirm = messagebox.askyesno("确认恢复",
"恢复密钥文件将覆盖当前的密钥文件。\n确定要继续吗?")
if not confirm:
return
file_path = filedialog.askopenfilename(
title="选择密钥文件备份",
filetypes=[("密钥文件", "*.key"), ("所有文件", "*.*")]
)
if file_path:
try:
import shutil
# 备份当前密钥文件
if os.path.exists(self.password_manager.key_path):
backup_name = f"secret_old_{datetime.now().strftime('%Y%m%d_%H%M%S')}.key"
shutil.copy2(self.password_manager.key_path, backup_name)
# 恢复密钥文件
shutil.copy2(file_path, self.password_manager.key_path)
messagebox.showinfo("恢复成功", "密钥文件已恢复!\n程序将重新启动以应用更改。")
# 重新启动程序
self.root.quit()
except Exception as e:
messagebox.showerror("恢复失败", f"无法恢复密钥文件:{str(e)}")
def show_key_recovery_guide(self):
"""显示密钥恢复指南"""
guide_dialog = tk.Toplevel(self.root)
guide_dialog.title('🔑 密钥文件恢复指南')
guide_dialog.geometry('700x500')
guide_dialog.transient(self.root)
frame = ttk.Frame(guide_dialog, padding='20')
frame.pack(fill=tk.BOTH, expand=True)
# 标题
ttk.Label(frame, text='🔑 密钥文件恢复指南',
font=('Microsoft YaHei UI', 16, 'bold')).pack(pady=(0, 15))
# 创建滚动文本框
text_frame = ttk.Frame(frame)
text_frame.pack(fill=tk.BOTH, expand=True)
text_widget = tk.Text(text_frame, wrap=tk.WORD, font=('Microsoft YaHei UI', 10))
scrollbar = ttk.Scrollbar(text_frame, orient=tk.VERTICAL, command=text_widget.yview)
text_widget.configure(yscrollcommand=scrollbar.set)
# 指南内容
guide_content = """
🛡️ 预防措施
1. 定期备份密钥文件
• 使用菜单:文件 → 💾 备份密钥文件
• 保存到安全位置(云存储、U盘等)
• 建议每月备份一次
2. 多重备份策略
• 本地备份:不同硬盘分区
• 云端备份:云存储服务
• 物理备份:U盘或移动硬盘
⚠️ 密钥文件丢失时的恢复选项
选项1:🔑 使用主密码恢复
• 适用:记得主密码的情况
• 步骤:输入主密码 → 重新生成密钥文件
• 优点:完全恢复访问权限
选项2:📁 从备份恢复
• 适用:有密钥文件备份
• 步骤:选择备份文件 → 自动恢复
• 优点:最安全可靠的方式
选项3:🆕 创建新数据库
• 适用:无法通过其他方式恢复
• 注意:原有数据将无法访问
🔧 手动恢复方法
方法1:菜单恢复
文件 → 🔄 恢复密钥文件 → 选择备份
方法2:手动复制
将备份文件重命名为 secret.key → 复制到程序目录
📋 最佳实践
• 定期备份(每次重要修改后)
• 安全存储(不同位置、加密保护)
• 记录管理(位置、日期、命名规范)
🚨 紧急情况
如果忘记主密码且没有备份:
1. 尝试回忆密码变体
2. 检查其他设备备份
3. 最后选择创建新数据库
🔒 安全提醒
• 密钥文件是访问密码的唯一凭证
• 不要通过不安全渠道传输
• 定期更换主密码
• 使用强密码作为主密码
重要:请务必做好密钥文件的备份工作!
"""
text_widget.insert(tk.END, guide_content)
text_widget.config(state=tk.DISABLED)
text_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 按钮框架
btn_frame = ttk.Frame(frame)
btn_frame.pack(pady=15)
ttk.Button(btn_frame, text='💾 立即备份密钥', command=self.backup_key_file, width=20).pack(side=tk.LEFT, padx=10)
ttk.Button(btn_frame, text='关闭', command=guide_dialog.destroy, width=15).pack(side=tk.LEFT, padx=10)
def change_theme(self, theme_name):
"""切换主题"""
if theme_name == 'light':
# 浅色主题
self.root.configure(bg='#f8f9fa')
self.style.configure('TLabel', background='#f8f9fa', foreground='#212529')
self.style.configure('TFrame', background='#f8f9fa')
self.style.configure('Treeview', background='white', foreground='#212529')
self.style.configure('Title.TLabel', background='#f8f9fa', foreground='#2c3e50')
# 按钮样式
self.style.configure('TButton',
background='#ffffff',
foreground='#212529',
borderwidth=1,
focuscolor='none')
self.style.map('TButton',
background=[('active', '#e9ecef'), ('pressed', '#dee2e6')],
foreground=[('active', '#212529'), ('pressed', '#212529')])
elif theme_name == 'dark':
# 深色主题
self.root.configure(bg='#2b2b2b')
self.style.configure('TLabel', background='#2b2b2b', foreground='#ffffff')
self.style.configure('TFrame', background='#2b2b2b')
self.style.configure('Treeview', background='#3c3c3c', foreground='#ffffff')
self.style.configure('Title.TLabel', background='#2b2b2b', foreground='#ffffff')
# 按钮样式
self.style.configure('TButton',
background='#404040',
foreground='#ffffff',
borderwidth=1,
focuscolor='none')
self.style.map('TButton',
background=[('active', '#505050'), ('pressed', '#606060')],
foreground=[('active', '#ffffff'), ('pressed', '#ffffff')])
elif theme_name == 'blue':
# 蓝色主题
self.root.configure(bg='#e3f2fd')
self.style.configure('TLabel', background='#e3f2fd', foreground='#0d47a1')
self.style.configure('TFrame', background='#e3f2fd')
self.style.configure('Treeview', background='#f3e5f5', foreground='#0d47a1')
self.style.configure('Title.TLabel', background='#e3f2fd', foreground='#1565c0')
# 按钮样式
self.style.configure('TButton',
background='#bbdefb',
foreground='#0d47a1',
borderwidth=1,
focuscolor='none')
self.style.map('TButton',
background=[('active', '#90caf9'), ('pressed', '#64b5f6')],
foreground=[('active', '#0d47a1'), ('pressed', '#0d47a1')])
elif theme_name == 'green':
# 绿色主题(护眼绿色调)
self.root.configure(bg='#e8f5e8')
self.style.configure('TLabel', background='#e8f5e8', foreground='#1b5e20')
self.style.configure('TFrame', background='#e8f5e8')
self.style.configure('Treeview', background='#f1f8e9', foreground='#2e7d32')
self.style.configure('Title.TLabel', background='#e8f5e8', foreground='#388e3c')
# 按钮样式
self.style.configure('TButton',
background='#c8e6c9',
foreground='#1b5e20',
borderwidth=1,
focuscolor='none')
self.style.map('TButton',
background=[('active', '#a5d6a7'), ('pressed', '#81c784')],
foreground=[('active', '#1b5e20'), ('pressed', '#1b5e20')])
elif theme_name == 'purple':
# 紫色主题(优雅紫色调)
self.root.configure(bg='#f3e5f5')
self.style.configure('TLabel', background='#f3e5f5', foreground='#4a148c')
self.style.configure('TFrame', background='#f3e5f5')
self.style.configure('Treeview', background='#fce4ec', foreground='#6a1b9a')
self.style.configure('Title.TLabel', background='#f3e5f5', foreground='#7b1fa2')
# 按钮样式
self.style.configure('TButton',
background='#e1bee7',
foreground='#4a148c',
borderwidth=1,
focuscolor='none')
self.style.map('TButton',
background=[('active', '#ce93d8'), ('pressed', '#ba68c8')],
foreground=[('active', '#4a148c'), ('pressed', '#4a148c')])
elif theme_name == 'orange':
# 橙色主题(温暖橙色调)
self.root.configure(bg='#fff3e0')
self.style.configure('TLabel', background='#fff3e0', foreground='#e65100')
self.style.configure('TFrame', background='#fff3e0')
self.style.configure('Treeview', background='#fef7f0', foreground='#f57c00')
self.style.configure('Title.TLabel', background='#fff3e0', foreground='#ff9800')
# 按钮样式
self.style.configure('TButton',
background='#ffcc80',
foreground='#e65100',
borderwidth=1,
focuscolor='none')
self.style.map('TButton',
background=[('active', '#ffb74d'), ('pressed', '#ffa726')],
foreground=[('active', '#e65100'), ('pressed', '#e65100')])
elif theme_name == 'high_contrast':
# 高对比度主题(适合视力不佳用户)
self.root.configure(bg='#ffffff')
self.style.configure('TLabel', background='#ffffff', foreground='#000000')
self.style.configure('TFrame', background='#ffffff')
self.style.configure('Treeview', background='#ffffff', foreground='#000000')
self.style.configure('Title.TLabel', background='#ffffff', foreground='#000000')
# 设置高对比度的选中颜色
self.style.configure('Treeview', selectbackground='#000000', selectforeground='#ffffff')
# 按钮样式
self.style.configure('TButton',
background='#ffffff',
foreground='#000000',
borderwidth=2,
relief='solid',
focuscolor='none')
self.style.map('TButton',
background=[('active', '#000000'), ('pressed', '#333333')],
foreground=[('active', '#ffffff'), ('pressed', '#ffffff')])
elif theme_name == 'night':
# 夜间模式主题(深黑背景,减少蓝光)
self.root.configure(bg='#0d1117')
self.style.configure('TLabel', background='#0d1117', foreground='#f0f6fc')
self.style.configure('TFrame', background='#0d1117')
self.style.configure('Treeview', background='#161b22', foreground='#f0f6fc')
self.style.configure('Title.TLabel', background='#0d1117', foreground='#58a6ff')
# 设置夜间模式的选中颜色
self.style.configure('Treeview', selectbackground='#21262d', selectforeground='#f0f6fc')
# 按钮样式
self.style.configure('TButton',
background='#21262d',
foreground='#f0f6fc',
borderwidth=1,
focuscolor='none')
self.style.map('TButton',
background=[('active', '#30363d'), ('pressed', '#484f58')],
foreground=[('active', '#f0f6fc'), ('pressed', '#f0f6fc')])
# 保存主题设置
try:
with open(os.path.join(os.getcwd(), 'theme.json'), 'w') as f:
json.dump({'theme': theme_name}, f)
except:
pass
def load_theme(self):
"""加载保存的主题"""
try:
theme_file = os.path.join(os.getcwd(), 'theme.json')
if os.path.exists(theme_file):
with open(theme_file, 'r') as f:
theme_data = json.load(f)
self.change_theme(theme_data.get('theme', 'blue'))
except:
self.change_theme('blue') # 默认蓝色主题
def sort_column(self, col):
"""列排序功能"""
# 获取当前数据
data = [(self.password_tree.set(child, col), child) for child in self.password_tree.get_children('')]
# 排序
reverse = self.sort_reverse[col]
if col in ['ID']:
# 数字排序
data.sort(key=lambda x: int(x[0]) if x[0].isdigit() else 0, reverse=reverse)
elif col in ['创建时间', '更新时间']:
# 时间排序
data.sort(key=lambda x: x[0], reverse=reverse)
else:
# 文本排序
data.sort(key=lambda x: x[0].lower(), reverse=reverse)
# 重新排列
for index, (val, child) in enumerate(data):
self.password_tree.move(child, '', index)
# 更新排序状态
self.sort_reverse[col] = not reverse
# 更新列标题显示排序方向
for column in self.password_tree['columns']:
if column == col:
direction = ' ↓' if reverse else ' ↑'
self.password_tree.heading(column, text=column + direction)
else:
self.password_tree.heading(column, text=column)
def show_shortcut_settings(self):
"""显示快捷键设置对话框"""
dialog = tk.Toplevel(self.root)
dialog.title('⌨️ 快捷键设置')
dialog.geometry('600x500')
dialog.resizable(False, False)
dialog.transient(self.root)
dialog.grab_set()
dialog.configure(bg='#f8f9fa')
# 居中显示
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
# 创建主框架
main_frame = ttk.Frame(dialog, padding='20')
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题
title_label = ttk.Label(main_frame, text='⌨️ 快捷键设置',
font=('Microsoft YaHei UI', 16, 'bold'),
foreground='#2c3e50')
title_label.pack(pady=(0, 20))
# 说明文字
info_label = ttk.Label(main_frame,
text='点击快捷键输入框,然后按下新的组合键来设置快捷键',
font=('Microsoft YaHei UI', 10),
foreground='#7f8c8d')
info_label.pack(pady=(0, 15))
# 创建滚动框架
canvas = tk.Canvas(main_frame, bg='#f8f9fa', highlightthickness=0)
scrollbar = ttk.Scrollbar(main_frame, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# 快捷键设置项
shortcut_entries = {}
action_names = {
'add_password': '添加密码',
'view_password': '查看密码',
'edit_password': '编辑密码',
'delete_password': '删除密码',
'generate_password': '生成密码',
'search': '搜索',
'lock_app': '锁定应用',
'unlock_app': '解锁应用',
'import_csv': '导入CSV',
'export_csv': '导出CSV',
'reset_search': '重置搜索',
'refresh': '刷新',
'copy_password': '复制密码',
'toggle_floating': '悬浮窗模式'
}
row = 0
for action, name in action_names.items():
# 功能名称
name_label = ttk.Label(scrollable_frame, text=name,
font=('Microsoft YaHei UI', 10, 'bold'))
name_label.grid(row=row, column=0, sticky=tk.W, padx=(0, 20), pady=8)
# 快捷键输入框
shortcut_var = tk.StringVar(value=self.password_manager.shortcut_manager.get_shortcut(action))
shortcut_entry = ttk.Entry(scrollable_frame, textvariable=shortcut_var,
width=15, font=('Microsoft YaHei UI', 10))
shortcut_entry.grid(row=row, column=1, padx=(0, 10), pady=8)
# 绑定键盘事件
shortcut_entry.bind('<KeyPress>', lambda e, action=action, var=shortcut_var:
self.capture_shortcut(e, action, var, dialog))
shortcut_entry.bind('<FocusIn>', lambda e: e.widget.select_range(0, tk.END))
shortcut_entries[action] = shortcut_var
# 清除按钮
clear_btn = ttk.Button(scrollable_frame, text='清除', width=8,
command=lambda a=action, v=shortcut_var: v.set(''))
clear_btn.grid(row=row, column=2, padx=5, pady=8)
row += 1
canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
# 按钮框架
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill=tk.X, pady=(20, 0))
def save_shortcuts():
"""保存快捷键设置"""
# 检查冲突
conflicts = []
for action1, var1 in shortcut_entries.items():
shortcut1 = var1.get().strip()
if shortcut1:
for action2, var2 in shortcut_entries.items():
if action1 != action2 and var2.get().strip() == shortcut1:
conflicts.append(f"{action_names[action1]} 和 {action_names[action2]} 使用了相同的快捷键: {shortcut1}")
if conflicts:
messagebox.showerror('快捷键冲突', '\n'.join(conflicts))
return
# 保存设置
for action, var in shortcut_entries.items():
self.password_manager.shortcut_manager.set_shortcut(action, var.get().strip())
if self.password_manager.shortcut_manager.save_shortcuts():
messagebox.showinfo('成功', '快捷键设置已保存!')
self.update_shortcuts() # 更新快捷键绑定
dialog.destroy()
else:
messagebox.showerror('错误', '保存快捷键设置失败!')
def reset_defaults():
"""重置为默认快捷键"""
if messagebox.askyesno('确认', '确定要重置为默认快捷键吗?'):
for action, var in shortcut_entries.items():
default_shortcut = self.password_manager.shortcut_manager.default_shortcuts.get(action, '')
var.set(default_shortcut)
# 按钮
ttk.Button(btn_frame, text='💾 保存', command=save_shortcuts, width=15).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text='🔄 重置默认', command=reset_defaults, width=15).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text='❌ 取消', command=dialog.destroy, width=15).pack(side=tk.RIGHT, padx=5)
def capture_shortcut(self, event, action, var, dialog):
"""捕获快捷键输入"""
# 阻止默认行为
event.widget.focus_set()
# 构建快捷键字符串
modifiers = []
if event.state & 0x4: # Control
modifiers.append('Ctrl')
if event.state & 0x8: # Alt
modifiers.append('Alt')
if event.state & 0x1: # Shift
modifiers.append('Shift')
key = event.keysym
# 特殊键处理
if key in ['Control_L', 'Control_R', 'Alt_L', 'Alt_R', 'Shift_L', 'Shift_R']:
return "break"
# 功能键
if key.startswith('F') and key[1:].isdigit():
shortcut = key
elif len(key) == 1 and key.isalnum():
if modifiers:
shortcut = '+'.join(modifiers) + '+' + key.upper()
else:
shortcut = key.upper()
else:
return "break"
var.set(shortcut)
return "break"
def update_shortcuts(self):
"""更新快捷键绑定"""
# 清除所有现有绑定
self.root.unbind_all('<Alt-q>')
self.root.unbind_all('<Alt-w>')
self.root.unbind_all('<Alt-e>')
self.root.unbind_all('<Alt-d>')
self.root.unbind_all('<Alt-g>')
self.root.unbind_all('<Alt-f>')
self.root.unbind_all('<Alt-l>')
self.root.unbind_all('<Alt-i>')
self.root.unbind_all('<Alt-o>')
self.root.unbind_all('<Alt-r>')
self.root.unbind_all('<F5>')
self.root.unbind_all('<Alt-c>')
self.root.unbind_all('<Alt-Escape>')
# 重新绑定快捷键
self.bind_shortcuts()
def toggle_floating_mode(self):
"""切换悬浮窗模式"""
if self.is_floating:
self.exit_floating_mode()
else:
self.enter_floating_mode()
def enter_floating_mode(self):
"""进入悬浮窗模式"""
if self.is_floating:
return
# 保存主窗口位置
self.main_window_geometry = self.root.geometry()
# 隐藏主窗口
self.root.withdraw()
# 创建悬浮窗
self.create_floating_window()
self.is_floating = True
def exit_floating_mode(self):
"""退出悬浮窗模式"""
if not self.is_floating:
return
# 保存悬浮窗位置
if self.floating_window:
self.save_floating_position()
self.floating_window.destroy()
self.floating_window = None
# 恢复主窗口
self.root.deiconify()
if hasattr(self, 'main_window_geometry'):
self.root.geometry(self.main_window_geometry)
self.is_floating = False
def create_floating_window(self):
"""创建悬浮窗"""
self.floating_window = tk.Toplevel()
self.floating_window.title('') # 无标题
self.floating_window.overrideredirect(True) # 无边框
# 初始为小球状态
self.floating_expanded = False
self.floating_window.geometry('60x60')
# 设置窗口属性
self.floating_window.attributes('-topmost', True) # 置顶
self.floating_window.attributes('-alpha', 0.8) # 半透明
# 设置位置
self.floating_window.geometry(f"60x60+{self.floating_position['x']}+{self.floating_position['y']}")
# 创建悬浮窗内容
self.create_floating_content()
# 绑定拖拽事件
self.bind_floating_drag()
def create_floating_content(self):
"""创建悬浮窗内容"""
# 清空现有内容
for widget in self.floating_window.winfo_children():
widget.destroy()
if not self.floating_expanded:
# 小球状态 - 只显示一个圆形按钮
self.create_ball_content()
else:
# 展开状态 - 显示完整功能
self.create_expanded_content()
def create_ball_content(self):
"""创建小球状态的内容"""
# 创建圆形按钮
ball_frame = tk.Frame(self.floating_window, bg='#4CAF50', width=60, height=60)
ball_frame.pack_propagate(False)
ball_frame.pack(fill=tk.BOTH, expand=True)
# 圆形效果(通过设置圆角边框模拟)
ball_frame.configure(relief='raised', bd=2)
# 中心图标
icon_label = tk.Label(ball_frame, text='🔐', font=('Microsoft YaHei UI', 20),
bg='#4CAF50', fg='white', cursor='hand2')
icon_label.place(relx=0.5, rely=0.5, anchor='center')
# 绑定点击事件
def toggle_expand(event=None):
self.toggle_floating_expand()
ball_frame.bind('<Button-1>', toggle_expand)
icon_label.bind('<Button-1>', toggle_expand)
# 绑定右键菜单
def show_context_menu(event):
context_menu = tk.Menu(self.floating_window, tearoff=0)
context_menu.add_command(label="📖 展开", command=self.toggle_floating_expand)
context_menu.add_command(label="➕ 添加密码", command=self.floating_add_password)
context_menu.add_separator()
context_menu.add_command(label="🔙 返回主窗口", command=self.exit_floating_mode)
try:
context_menu.tk_popup(event.x_root, event.y_root)
finally:
context_menu.grab_release()
ball_frame.bind('<Button-3>', show_context_menu)
icon_label.bind('<Button-3>', show_context_menu)
def create_expanded_content(self):
"""创建展开状态的内容"""
# 主框架
main_frame = tk.Frame(self.floating_window, bg='#f0f0f0', padx=10, pady=10)
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题栏
title_frame = tk.Frame(main_frame, bg='#f0f0f0')
title_frame.pack(fill=tk.X, pady=(0, 10))
title_label = tk.Label(title_frame, text='🔐 密码管理器',
font=('Microsoft YaHei UI', 10, 'bold'), bg='#f0f0f0')
title_label.pack(side=tk.LEFT)
# 收起按钮
collapse_btn = tk.Button(title_frame, text='📖', font=('Microsoft YaHei UI', 8),
command=self.toggle_floating_expand, bg='#ff9800', fg='white',
width=3, height=1, relief='flat', cursor='hand2')
collapse_btn.pack(side=tk.RIGHT)
# 快速搜索
search_frame = tk.Frame(main_frame, bg='#f0f0f0')
search_frame.pack(fill=tk.X, pady=(0, 10))
self.floating_search_var = tk.StringVar()
search_entry = tk.Entry(search_frame, textvariable=self.floating_search_var,
font=('Microsoft YaHei UI', 9), width=25)
search_entry.pack(fill=tk.X)
search_entry.bind('<Return>', lambda e: self.floating_search())
# 功能选择标签页
tab_frame = tk.Frame(main_frame, bg='#f0f0f0')
tab_frame.pack(fill=tk.X, pady=(0, 10))
# 标签页按钮 - 存储为实例变量以便后续更新
self.password_tab_btn = tk.Button(tab_frame, text='🔐 密码',
command=lambda: self.switch_floating_tab('passwords'),
font=('Microsoft YaHei UI', 8), width=10,
bg='#2196F3' if self.floating_current_tab == 'passwords' else '#e0e0e0',
fg='white' if self.floating_current_tab == 'passwords' else 'black')
self.password_tab_btn.pack(side=tk.LEFT, padx=2)
self.text_tab_btn = tk.Button(tab_frame, text='📝 文本',
command=lambda: self.switch_floating_tab('texts'),
font=('Microsoft YaHei UI', 8), width=10,
bg='#2196F3' if self.floating_current_tab == 'texts' else '#e0e0e0',
fg='white' if self.floating_current_tab == 'texts' else 'black')
self.text_tab_btn.pack(side=tk.LEFT, padx=2)
# 快速操作按钮
btn_frame = tk.Frame(main_frame, bg='#f0f0f0')
btn_frame.pack(fill=tk.X, pady=(0, 10))
self.floating_add_btn = tk.Button(btn_frame, text='➕ 添加', command=self.floating_add_item,
font=('Microsoft YaHei UI', 8), width=8, bg='#4CAF50', fg='white')
self.floating_add_btn.pack(side=tk.LEFT, padx=2)
self.floating_search_btn = tk.Button(btn_frame, text='🔍 搜索', command=self.floating_search,
font=('Microsoft YaHei UI', 8), width=8, bg='#2196F3', fg='white')
self.floating_search_btn.pack(side=tk.RIGHT, padx=2)
# 密码/文本列表
list_frame = tk.Frame(main_frame, bg='#f0f0f0')
list_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
# 创建列表框
self.floating_listbox = tk.Listbox(list_frame, height=8,
font=('Microsoft YaHei UI', 9),
selectmode=tk.SINGLE)
# 添加滚动条
scrollbar = tk.Scrollbar(list_frame, orient=tk.VERTICAL, command=self.floating_listbox.yview)
self.floating_listbox.configure(yscrollcommand=scrollbar.set)
# 布局
self.floating_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 绑定事件
self.floating_listbox.bind('<Double-1>', self.on_floating_double_click)
self.floating_listbox.bind('<Button-3>', self.show_floating_context_menu)
# 底部按钮
bottom_frame = tk.Frame(main_frame, bg='#f0f0f0')
bottom_frame.pack(fill=tk.X)
tk.Button(bottom_frame, text='📋 复制', command=self.floating_copy_password,
font=('Microsoft YaHei UI', 8), width=8, bg='#FF9800', fg='white').pack(side=tk.LEFT, padx=2)
tk.Button(bottom_frame, text='🔙 返回', command=self.exit_floating_mode,
font=('Microsoft YaHei UI', 8), width=8, bg='#f44336', fg='white').pack(side=tk.RIGHT, padx=2)
# 更新悬浮窗列表
self.update_floating_list()
def switch_floating_tab(self, tab_name):
"""切换悬浮窗标签页"""
self.floating_current_tab = tab_name
# 只更新列表内容,不重新创建整个界面
self.update_floating_list()
# 更新标签页按钮状态
self.update_tab_buttons()
def update_tab_buttons(self):
"""更新标签页按钮状态"""
if hasattr(self, 'password_tab_btn') and hasattr(self, 'text_tab_btn'):
if self.floating_current_tab == 'passwords':
self.password_tab_btn.config(bg='#2196F3', fg='white')
self.text_tab_btn.config(bg='#e0e0e0', fg='black')
else:
self.password_tab_btn.config(bg='#e0e0e0', fg='black')
self.text_tab_btn.config(bg='#2196F3', fg='white')
def toggle_floating_expand(self):
"""切换悬浮窗展开/收起状态"""
self.floating_expanded = not self.floating_expanded
if self.floating_expanded:
# 展开到完整大小
self.floating_window.geometry('250x350')
else:
# 收起到小球大小
self.floating_window.geometry('60x60')
# 重新创建内容
self.create_floating_content()
def bind_floating_drag(self):
"""绑定悬浮窗拖拽事件"""
def start_drag(event):
self.floating_window.x = event.x
self.floating_window.y = event.y
def on_drag(event):
x = self.floating_window.winfo_pointerx() - self.floating_window.x
y = self.floating_window.winfo_pointery() - self.floating_window.y
self.floating_window.geometry(f"+{x}+{y}")
# 绑定到整个窗口(因为使用了 overrideredirect)
self.floating_window.bind('<Button-1>', start_drag)
self.floating_window.bind('<B1-Motion>', on_drag)
# 绑定到所有子组件
def bind_to_children(widget):
widget.bind('<Button-1>', start_drag, add='+')
widget.bind('<B1-Motion>', on_drag, add='+')
for child in widget.winfo_children():
if not isinstance(child, (tk.Button, tk.Entry, tk.Listbox)): # 排除交互组件
bind_to_children(child)
bind_to_children(self.floating_window)
def update_floating_list(self):
"""更新悬浮窗列表"""
# 只在展开状态下更新列表
if not self.floating_expanded or not hasattr(self, 'floating_listbox'):
return
# 清空现有项
self.floating_listbox.delete(0, tk.END)
if self.floating_current_tab == 'passwords':
passwords = self.password_manager.get_all_passwords()
self.floating_data = passwords[:20] # 存储数据用于后续操作
for password in self.floating_data:
display_text = f"🔐 {password[1]} - {password[2]}" # 服务名 - 用户名
if password[3]: # 如果有URL
display_text += " 🌐"
self.floating_listbox.insert(tk.END, display_text)
elif self.floating_current_tab == 'texts':
texts = self.password_manager.get_all_encrypted_texts('title', 'ASC')
self.floating_data = texts[:20] # 存储数据用于后续操作
for text in self.floating_data:
# 确保有足够的字段
if len(text) >= 4:
title = text[1] if text[1] else "无标题"
created_time = text[2] if text[2] else "未知时间"
# 只显示日期部分,避免显示过长
time_display = created_time[:10] if len(created_time) >= 10 else created_time
display_text = f"📝 {title} - {time_display}"
else:
display_text = f"📝 {text[1] if len(text) > 1 else '未知文本'}"
self.floating_listbox.insert(tk.END, display_text)
def on_floating_double_click(self, event):
"""悬浮窗双击事件处理"""
selection = self.floating_listbox.curselection()
if not selection or not hasattr(self, 'floating_data'):
return
index = selection[0]
if index >= len(self.floating_data):
return
if self.floating_current_tab == 'passwords':
# 双击复制密码
password_info = self.floating_data[index]
service, username = password_info[1], password_info[2]
password = self.password_manager.get_password(service, username)
if password:
pyperclip.copy(password)
messagebox.showinfo('✅ 成功', f'{service} 的密码已复制到剪贴板!')
elif self.floating_current_tab == 'texts':
# 双击查看文本
text_info = self.floating_data[index]
text_id = text_info[0]
title, content = self.password_manager.get_encrypted_text(text_id)
if content:
# 临时显示主窗口来查看文本
self.root.deiconify()
self.show_text_content_dialog(title, content)
self.root.withdraw()
def show_floating_context_menu(self, event):
"""显示悬浮窗右键菜单"""
selection = self.floating_listbox.curselection()
if not selection or not hasattr(self, 'floating_data'):
return
index = selection[0]
if index >= len(self.floating_data):
return
context_menu = tk.Menu(self.floating_window, tearoff=0)
if self.floating_current_tab == 'passwords':
password_info = self.floating_data[index]
service, username, url = password_info[1], password_info[2], password_info[3]
context_menu.add_command(label="📋 复制密码",
command=lambda: self.copy_floating_password(service, username))
if url:
context_menu.add_command(label="🌐 打开URL",
command=lambda: self.open_floating_url(url))
context_menu.add_separator()
context_menu.add_command(label="👁️ 查看详情",
command=lambda: self.view_floating_password_details(password_info))
elif self.floating_current_tab == 'texts':
text_info = self.floating_data[index]
text_id, title = text_info[0], text_info[1]
context_menu.add_command(label="👁️ 查看内容",
command=lambda: self.view_floating_text(text_id))
context_menu.add_command(label="📋 复制内容",
command=lambda: self.copy_floating_text(text_id))
try:
context_menu.tk_popup(event.x_root, event.y_root)
finally:
context_menu.grab_release()
def copy_floating_password(self, service, username):
"""复制悬浮窗密码"""
password = self.password_manager.get_password(service, username)
if password:
pyperclip.copy(password)
messagebox.showinfo('✅ 成功', f'{service} 的密码已复制到剪贴板!')
def open_floating_url(self, url):
"""打开悬浮窗URL"""
import webbrowser
webbrowser.open(url)
def view_floating_password_details(self, password_info):
"""查看悬浮窗密码详情"""
# 临时显示主窗口
self.root.deiconify()
# 这里可以调用现有的查看密码方法
messagebox.showinfo('密码详情', f'服务: {password_info[1]}\n用户名: {password_info[2]}\nURL: {password_info[3] or "无"}')
self.root.withdraw()
def view_floating_text(self, text_id):
"""查看悬浮窗文本"""
title, content = self.password_manager.get_encrypted_text(text_id)
if content:
# 临时显示主窗口来查看文本
self.root.deiconify()
self.show_text_content_dialog(title, content)
self.root.withdraw()
def copy_floating_text(self, text_id):
"""复制悬浮窗文本"""
title, content = self.password_manager.get_encrypted_text(text_id)
if content:
pyperclip.copy(content)
messagebox.showinfo('✅ 成功', f'{title} 的内容已复制到剪贴板!')
def floating_search(self):
"""悬浮窗搜索功能"""
# 只在展开状态下搜索
if not self.floating_expanded or not hasattr(self, 'floating_search_var'):
return
keyword = self.floating_search_var.get().strip()
if not keyword:
self.update_floating_list()
return
# 清空现有项
self.floating_listbox.delete(0, tk.END)
if self.floating_current_tab == 'passwords':
results = self.password_manager.search_passwords(keyword)
self.floating_data = results[:20] # 存储搜索结果
for password in self.floating_data:
display_text = f"🔐 {password[1]} - {password[2]}" # 服务名 - 用户名
if password[3]: # 如果有URL
display_text += " 🌐"
self.floating_listbox.insert(tk.END, display_text)
elif self.floating_current_tab == 'texts':
results = self.password_manager.search_encrypted_texts(keyword)
self.floating_data = results[:20] # 存储搜索结果
for text in self.floating_data:
# 确保有足够的字段
if len(text) >= 4:
title = text[1] if text[1] else "无标题"
created_time = text[2] if text[2] else "未知时间"
# 只显示日期部分,避免显示过长
time_display = created_time[:10] if len(created_time) >= 10 else created_time
display_text = f"📝 {title} - {time_display}"
else:
display_text = f"📝 {text[1] if len(text) > 1 else '未知文本'}"
self.floating_listbox.insert(tk.END, display_text)
def floating_add_item(self):
"""悬浮窗添加项目"""
if self.floating_current_tab == 'passwords':
self.floating_add_password()
elif self.floating_current_tab == 'texts':
self.floating_add_text()
def floating_add_password(self):
"""悬浮窗添加密码"""
# 临时显示主窗口来添加密码
self.root.deiconify()
self.add_password_dialog()
self.root.withdraw()
# 更新悬浮窗列表
self.update_floating_list()
def floating_add_text(self):
"""悬浮窗添加文本"""
# 临时显示主窗口来添加文本
self.root.deiconify()
self.show_add_text_dialog()
self.root.withdraw()
# 更新悬浮窗列表
self.update_floating_list()
def floating_copy_password(self):
"""悬浮窗复制密码 - 现在通过列表项直接操作"""
messagebox.showinfo('提示', '请点击密码项右侧的📋图标来复制密码')
def save_floating_position(self):
"""保存悬浮窗位置"""
if self.floating_window:
geometry = self.floating_window.geometry()
# 解析几何字符串 "200x300+100+100"
parts = geometry.split('+')
if len(parts) >= 3:
self.floating_position['x'] = int(parts[1])
self.floating_position['y'] = int(parts[2])
# 保存到文件
try:
config_file = os.path.join(os.getcwd(), 'floating_config.json')
with open(config_file, 'w') as f:
json.dump(self.floating_position, f)
except:
pass
def load_floating_position(self):
"""加载悬浮窗位置"""
try:
config_file = os.path.join(os.getcwd(), 'floating_config.json')
if os.path.exists(config_file):
with open(config_file, 'r') as f:
self.floating_position = json.load(f)
except:
# 使用默认位置
self.floating_position = {'x': 100, 'y': 100}
def show_text_manager(self):
"""显示文本管理器窗口"""
dialog = tk.Toplevel(self.root)
dialog.title('📝 加密文本管理器')
dialog.geometry('800x600')
dialog.resizable(True, True)
dialog.transient(self.root)
dialog.grab_set()
dialog.configure(bg='#f8f9fa')
# 居中显示
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
# 创建主框架
main_frame = ttk.Frame(dialog, padding='15')
main_frame.pack(fill=tk.BOTH, expand=True)
# 标题
title_label = ttk.Label(main_frame, text='📝 加密文本管理器',
font=('Microsoft YaHei UI', 16, 'bold'),
foreground='#2c3e50')
title_label.pack(pady=(0, 20))
# 按钮框架 - 优化布局,更紧凑
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Button(button_frame, text='➕ 添加文本', command=lambda: self.show_add_text_dialog(dialog),
width=12).pack(side=tk.LEFT, padx=3)
ttk.Button(button_frame, text='👁️ 查看文本', command=lambda: self.view_selected_text(text_tree, dialog),
width=12).pack(side=tk.LEFT, padx=3)
ttk.Button(button_frame, text='✏️ 编辑文本', command=lambda: self.edit_selected_text(text_tree, dialog),
width=12).pack(side=tk.LEFT, padx=3)
ttk.Button(button_frame, text='🗑️ 删除文本', command=lambda: self.delete_selected_text(text_tree),
width=12).pack(side=tk.LEFT, padx=3)
# 添加分隔符
ttk.Separator(button_frame, orient='vertical').pack(side=tk.LEFT, fill=tk.Y, padx=8)
# 导出导入按钮
ttk.Button(button_frame, text='📤 导出CSV', command=lambda: self.export_texts_dialog(text_tree),
width=12).pack(side=tk.LEFT, padx=3)
ttk.Button(button_frame, text='📥 导入CSV', command=lambda: self.import_texts_dialog(text_tree),
width=12).pack(side=tk.LEFT, padx=3)
# 搜索和排序框架 - 优化布局,更紧凑
search_frame = ttk.Frame(main_frame)
search_frame.pack(fill=tk.X, pady=(0, 10))
# 搜索部分 - 缩小搜索框
ttk.Label(search_frame, text='🔍 搜索:', font=('Microsoft YaHei UI', 10)).pack(side=tk.LEFT, padx=(0, 5))
text_search_var = tk.StringVar()
text_search_entry = ttk.Entry(search_frame, textvariable=text_search_var,
font=('Microsoft YaHei UI', 9), width=20)
text_search_entry.pack(side=tk.LEFT, padx=(0, 8))
# 排序部分 - 减少间距
ttk.Label(search_frame, text='📊 排序:', font=('Microsoft YaHei UI', 10)).pack(side=tk.LEFT, padx=(10, 3))
sort_by_var = tk.StringVar(value='title')
sort_by_combo = ttk.Combobox(search_frame, textvariable=sort_by_var,
values=['id', 'title', 'created_at', 'updated_at'],
state='readonly', width=10, font=('Microsoft YaHei UI', 9))
sort_by_combo.pack(side=tk.LEFT, padx=(0, 3))
sort_order_var = tk.StringVar(value='ASC')
sort_order_combo = ttk.Combobox(search_frame, textvariable=sort_order_var,
values=['ASC', 'DESC'], state='readonly', width=6, font=('Microsoft YaHei UI', 9))
sort_order_combo.pack(side=tk.LEFT, padx=(0, 8))
def search_and_sort_texts():
keyword = text_search_var.get().strip()
sort_by = sort_by_var.get()
sort_order = sort_order_var.get()
if keyword:
results = self.password_manager.search_encrypted_texts(keyword)
# 对搜索结果进行排序
if sort_by == 'id':
results.sort(key=lambda x: x[0], reverse=(sort_order == 'DESC'))
elif sort_by == 'title':
results.sort(key=lambda x: x[1] or '', reverse=(sort_order == 'DESC'))
elif sort_by == 'created_at':
results.sort(key=lambda x: x[2] or '', reverse=(sort_order == 'DESC'))
elif sort_by == 'updated_at':
results.sort(key=lambda x: x[3] or '', reverse=(sort_order == 'DESC'))
else:
results = self.password_manager.get_all_encrypted_texts(sort_by, sort_order)
self.update_text_list(text_tree, results)
text_search_entry.bind('<Return>', lambda e: search_and_sort_texts())
sort_by_combo.bind('<<ComboboxSelected>>', lambda e: search_and_sort_texts())
sort_order_combo.bind('<<ComboboxSelected>>', lambda e: search_and_sort_texts())
# 按钮 - 缩小尺寸和间距
ttk.Button(search_frame, text='🔍 搜索', command=search_and_sort_texts, width=8).pack(side=tk.LEFT, padx=3)
ttk.Button(search_frame, text='🔄 重置',
command=lambda: [text_search_var.set(''), sort_by_var.set('title'), sort_order_var.set('ASC'), search_and_sort_texts()],
width=8).pack(side=tk.LEFT, padx=3)
# 文本列表
columns = ('ID', '标题', '创建时间', '更新时间')
text_tree = ttk.Treeview(main_frame, columns=columns, show='headings', selectmode='browse')
for col in columns:
text_tree.heading(col, text=col)
if col == 'ID':
text_tree.column(col, width=50, anchor=tk.CENTER)
elif col == '标题':
text_tree.column(col, width=300, anchor=tk.W)
else:
text_tree.column(col, width=150, anchor=tk.CENTER)
# 添加滚动条
text_scrollbar = ttk.Scrollbar(main_frame, orient=tk.VERTICAL, command=text_tree.yview)
text_tree.configure(yscroll=text_scrollbar.set)
text_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, pady=5)
text_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 绑定双击事件
text_tree.bind('<Double-1>', lambda e: self.view_selected_text(text_tree, dialog))
# 初始化文本列表(默认按标题升序排序)
texts = self.password_manager.get_all_encrypted_texts('title', 'ASC')
self.update_text_list(text_tree, texts)
def update_text_list(self, text_tree, texts):
"""更新文本列表"""
# 清空现有项
for item in text_tree.get_children():
text_tree.delete(item)
# 添加到列表
for text in texts:
text_tree.insert('', tk.END, values=text)
def update_time(self):
"""更新状态栏时间"""
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.time_var.set(current_time)
# 每秒更新一次
self.root.after(1000, self.update_time)
def show_add_text_dialog(self, parent=None):
"""显示添加文本对话框"""
if parent is None:
parent = self.root
dialog = tk.Toplevel(parent)
dialog.title('➕ 添加加密文本')
dialog.geometry('600x500')
dialog.resizable(True, True)
dialog.transient(parent)
dialog.grab_set()
# 居中显示
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
# 创建框架
frame = ttk.Frame(dialog, padding='25')
frame.pack(fill=tk.BOTH, expand=True)
# 标题输入
ttk.Label(frame, text='标题:', font=('Microsoft YaHei UI', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
title_var = tk.StringVar()
title_entry = ttk.Entry(frame, textvariable=title_var, font=('Microsoft YaHei UI', 10))
title_entry.pack(fill=tk.X, pady=(0, 15))
# 内容输入
ttk.Label(frame, text='内容:', font=('Microsoft YaHei UI', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
content_text = tk.Text(frame, font=('Microsoft YaHei UI', 10), wrap=tk.WORD)
content_text.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
# 添加滚动条
content_scrollbar = ttk.Scrollbar(content_text, orient=tk.VERTICAL, command=content_text.yview)
content_text.configure(yscrollcommand=content_scrollbar.set)
content_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 按钮框架
btn_frame = ttk.Frame(frame)
btn_frame.pack(fill=tk.X, pady=(15, 0))
def save_text():
title = title_var.get().strip()
content = content_text.get('1.0', tk.END).strip()
if self.password_manager.add_encrypted_text(title, content):
dialog.destroy()
# 如果有父窗口是文本管理器,刷新列表
if hasattr(parent, 'winfo_children'):
for child in parent.winfo_children():
if isinstance(child, ttk.Frame):
for grandchild in child.winfo_children():
if isinstance(grandchild, ttk.Treeview):
texts = self.password_manager.get_all_encrypted_texts('title', 'ASC')
self.update_text_list(grandchild, texts)
break
ttk.Button(btn_frame, text='💾 保存', command=save_text).pack(side=tk.LEFT, padx=10)
ttk.Button(btn_frame, text='❌ 取消', command=dialog.destroy).pack(side=tk.LEFT, padx=10)
# 设置焦点
title_entry.focus()
def view_selected_text(self, text_tree, parent):
"""查看选中的文本"""
selection = text_tree.selection()
if not selection:
messagebox.showwarning('提示', '请先选择一条文本记录!')
return
item = text_tree.item(selection[0])
text_id, title = item['values'][0], item['values'][1]
# 获取解密后的内容
_, content = self.password_manager.get_encrypted_text(text_id)
if content is None:
messagebox.showerror('错误', '无法获取文本内容!')
return
self.show_text_content_dialog(title, content, parent)
def show_text_content_dialog(self, title, content, parent=None):
"""显示文本内容对话框"""
if parent is None:
parent = self.root
dialog = tk.Toplevel(parent)
dialog.title(f'👁️ {title}')
dialog.geometry('700x500')
dialog.resizable(True, True)
dialog.transient(parent)
dialog.grab_set()
# 居中显示
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
# 创建框架
frame = ttk.Frame(dialog, padding='25')
frame.pack(fill=tk.BOTH, expand=True)
# 标题显示
title_label = ttk.Label(frame, text=f'📝 {title}',
font=('Microsoft YaHei UI', 14, 'bold'),
foreground='#2c3e50')
title_label.pack(pady=(0, 15))
# 内容显示
content_text = tk.Text(frame, font=('Microsoft YaHei UI', 10), wrap=tk.WORD, state=tk.DISABLED)
content_text.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
# 插入内容
content_text.config(state=tk.NORMAL)
content_text.insert('1.0', content)
content_text.config(state=tk.DISABLED)
# 添加滚动条
content_scrollbar = ttk.Scrollbar(content_text, orient=tk.VERTICAL, command=content_text.yview)
content_text.configure(yscrollcommand=content_scrollbar.set)
content_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 按钮框架
btn_frame = ttk.Frame(frame)
btn_frame.pack(fill=tk.X)
def copy_content():
pyperclip.copy(content)
messagebox.showinfo('✅ 成功', '内容已复制到剪贴板!')
ttk.Button(btn_frame, text='📋 复制内容', command=copy_content).pack(side=tk.LEFT, padx=10)
ttk.Button(btn_frame, text='❌ 关闭', command=dialog.destroy).pack(side=tk.RIGHT, padx=10)
def edit_selected_text(self, text_tree, parent):
"""编辑选中的文本"""
selection = text_tree.selection()
if not selection:
messagebox.showwarning('提示', '请先选择一条文本记录!')
return
item = text_tree.item(selection[0])
text_id, title = item['values'][0], item['values'][1]
# 获取解密后的内容
_, content = self.password_manager.get_encrypted_text(text_id)
if content is None:
messagebox.showerror('错误', '无法获取文本内容!')
return
self.show_edit_text_dialog(text_id, title, content, text_tree, parent)
def show_edit_text_dialog(self, text_id, title, content, text_tree, parent):
"""显示编辑文本对话框"""
dialog = tk.Toplevel(parent)
dialog.title('✏️ 编辑加密文本')
dialog.geometry('600x500')
dialog.resizable(True, True)
dialog.transient(parent)
dialog.grab_set()
# 居中显示
dialog.update_idletasks()
width = dialog.winfo_width()
height = dialog.winfo_height()
x = (dialog.winfo_screenwidth() // 2) - (width // 2)
y = (dialog.winfo_screenheight() // 2) - (height // 2)
dialog.geometry('{}x{}+{}+{}'.format(width, height, x, y))
# 创建框架
frame = ttk.Frame(dialog, padding='25')
frame.pack(fill=tk.BOTH, expand=True)
# 标题输入
ttk.Label(frame, text='标题:', font=('Microsoft YaHei UI', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
title_var = tk.StringVar(value=title)
title_entry = ttk.Entry(frame, textvariable=title_var, font=('Microsoft YaHei UI', 10))
title_entry.pack(fill=tk.X, pady=(0, 15))
# 内容输入
ttk.Label(frame, text='内容:', font=('Microsoft YaHei UI', 10, 'bold')).pack(anchor=tk.W, pady=(0, 5))
content_text = tk.Text(frame, font=('Microsoft YaHei UI', 10), wrap=tk.WORD)
content_text.pack(fill=tk.BOTH, expand=True, pady=(0, 15))
content_text.insert('1.0', content)
# 添加滚动条
content_scrollbar = ttk.Scrollbar(content_text, orient=tk.VERTICAL, command=content_text.yview)
content_text.configure(yscrollcommand=content_scrollbar.set)
content_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 按钮框架
btn_frame = ttk.Frame(frame)
btn_frame.pack(fill=tk.X, pady=(15, 0))
def update_text():
new_title = title_var.get().strip()
new_content = content_text.get('1.0', tk.END).strip()
if self.password_manager.update_encrypted_text(text_id, new_title, new_content):
dialog.destroy()
# 刷新文本列表
texts = self.password_manager.get_all_encrypted_texts('title', 'ASC')
self.update_text_list(text_tree, texts)
ttk.Button(btn_frame, text='💾 保存', command=update_text).pack(side=tk.LEFT, padx=10)
ttk.Button(btn_frame, text='❌ 取消', command=dialog.destroy).pack(side=tk.LEFT, padx=10)
# 设置焦点
title_entry.focus()
def export_texts_dialog(self, text_tree):
"""导出加密文本对话框"""
from datetime import datetime
from tkinter import filedialog
file_path = filedialog.asksaveasfilename(
title="导出加密文本",
defaultextension=".csv",
filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")],
initialfile=f"encrypted_texts_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
)
if file_path:
success, message = self.password_manager.export_texts_to_csv(file_path)
if success:
messagebox.showinfo("导出成功", message)
else:
messagebox.showerror("导出失败", message)
def import_texts_dialog(self, text_tree):
"""导入加密文本对话框"""
from tkinter import filedialog
file_path = filedialog.askopenfilename(
title="导入加密文本",
filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")]
)
if file_path:
confirm = messagebox.askyesno("确认导入",
"导入操作将添加新的文本记录。\n已存在相同标题的记录将被跳过。\n是否继续?")
if confirm:
success, message = self.password_manager.import_texts_from_csv(file_path)
if success:
messagebox.showinfo("导入完成", message)
# 刷新文本列表
texts = self.password_manager.get_all_encrypted_texts('title', 'ASC')
self.update_text_list(text_tree, texts)
else:
messagebox.showerror("导入失败", message)
def export_texts_menu(self):
"""从菜单导出文本"""
from datetime import datetime
from tkinter import filedialog
file_path = filedialog.asksaveasfilename(
title="导出加密文本",
defaultextension=".csv",
filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")],
initialfile=f"encrypted_texts_export_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
)
if file_path:
success, message = self.password_manager.export_texts_to_csv(file_path)
if success:
messagebox.showinfo("导出成功", message)
else:
messagebox.showerror("导出失败", message)
def import_texts_menu(self):
"""从菜单导入文本"""
from tkinter import filedialog
file_path = filedialog.askopenfilename(
title="导入加密文本",
filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")]
)
if file_path:
confirm = messagebox.askyesno("确认导入",
"导入操作将添加新的文本记录。\n已存在相同标题的记录将被跳过。\n是否继续?")
if confirm:
success, message = self.password_manager.import_texts_from_csv(file_path)
if success:
messagebox.showinfo("导入完成", message)
else:
messagebox.showerror("导入失败", message)
def delete_selected_text(self, text_tree):
"""删除选中的文本"""
selection = text_tree.selection()
if not selection:
messagebox.showwarning('提示', '请先选择一条文本记录!')
return
item = text_tree.item(selection[0])
text_id, title = item['values'][0], item['values'][1]
# 确认删除
if messagebox.askyesno('确认删除', f'确定要删除文本 "{title}" 吗?\n\n此操作不可撤销!'):
if self.password_manager.delete_encrypted_text(text_id):
# 刷新文本列表
texts = self.password_manager.get_all_encrypted_texts('title', 'ASC')
self.update_text_list(text_tree, texts)
if __name__ == '__main__':
root = tk.Tk()
app = PasswordManagerGUI(root)
app.load_theme() # 加载保存的主题
root.mainloop()