Tkinter密码管理器查询时触发cryptography.fernet.InvalidToken错误求助
Tkinter密码管理器:重启后解密失败(InvalidToken错误)
问题描述
开发基于Tkinter的GUI密码管理器,将加密后的密码存储到SQLite数据库,目标是通过网站名称查询返回解密后的密码。目前save()函数可正常保存加密密码,但调用find_password()时触发cryptography.fernet.InvalidToken错误,仅能在程序单次运行周期内完成加解密,重启后密钥变化导致解密失败。
原代码实现
import tkinter as tk from tkinter import messagebox import random import pyperclip import sqlite3 from werkzeug.security import generate_password_hash, check_password_hash import base64 import os from cryptography.fernet import Fernet from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC # DATABASE. db = sqlite3.connect('user-data.db') cursor = db.cursor() # cursor.execute('CREATE TABLE UserData (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, ' # 'website varchar(250) NOT NULL UNIQUE, ' # 'email varchar(250) NOT NULL, ' # 'password varchar(250) NOT NULL)') # cursor.execute('INSERT INTO UserData VALUES(1, "amazon.co.uk", "my_email@gmail.com", "wieuiwrhfg4")') # db.commit() # # key = Fernet.generate_key() # f = Fernet(key) # PASSWORD GENERATOR. def generate_password(): letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'] numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] symbols = ['!', '#', '$', '%', '&', '(', ')', '*', '+'] password_letters = [random.choice(letters) for _ in range(random.randint(8, 10))] password_symbols = [random.choice(symbols) for _ in range(random.randint(2, 4))] password_numbers = [random.choice(numbers) for _ in range(random.randint(2, 4))] password_list = password_letters + password_symbols + password_numbers random.shuffle(password_list) password = "".join(password_list) password_entry.insert(0, string=password) pyperclip.copy(password) return password def password_enc(): salt = os.urandom(16) kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=480000 ) key = base64.urlsafe_b64encode(kdf.derive(bytes(generate_password(), 'utf-8'))) # key = Fernet.generate_key().decode() f = Fernet(key) token = f.encrypt(bytes(generate_password(), 'utf-8')) return token, f # SAVE PASSWORD. def save(): website = website_entry.get() # .get() fetches the current entry text. password = password_entry.get() # hash_salted_password = generate_password_hash(password, method='pbkdf2:sha256', salt_length=8) # encrypted_password = f.encrypt(str.encode(password)) # encrypted_password = str.encode(password) # key = base64.urlsafe_b64encode(kdf.derive(str.encode(password))) # f = Fernet(key) # token = f.encrypt(str.encode(password)) # print(token) if len(website) == 0 or len(password) == 0: messagebox.showinfo(title="Oops", message="Please make sure you haven't left any fields empty.") else: try: cursor.execute("INSERT INTO UserData VALUES(?, ?, ?, ?)", (None, website_entry.get(), email_entry.get(), password_enc()[0])) db.commit() finally: website_entry.delete(0, tk.END) # .delete() from index 0, first char till the END. password_entry.delete(0, tk.END) # FIND PASSWORD. def find_password(): website = website_entry.get() try: cursor.execute("SELECT * FROM UserData WHERE website=?", (website,)) result = cursor.fetchall() print(result) except FileNotFoundError: messagebox.showinfo(title="Error", message="No Data File Found.") else: if website in result[0]: email = result[0][1] hashed_password = result[0][3] # password = check_password_hash(pwhash=hashed_password, password=generate_password()) decrypted_password = password_enc()[1].decrypt(hashed_password) # decrypted_password = save().decode() # decrypted_password = f.decrypt(hashed_password) messagebox.showinfo(title=website, message=f"Email: {email}\nPassword: {decrypted_password}") else: messagebox.showinfo(title="Error", message=f"No details for {website} exists.")
报错信息
Exception in Tkinter callback Traceback (most recent call last): File "/Users//PycharmProjects/pythonProject/Day-24/venv/lib/python3.10/site-packages/cryptography/fernet.py", line 133, in _verify_signature h.verify(data[-32:]) File "/Users//PycharmProjects/pythonProject/Day-24/venv/lib/python3.10/site-packages/cryptography/hazmat/primitives/hmac.py", line 72, in verify ctx.verify(signature) File "/Users//PycharmProjects/pythonProject/Day-24/venv/lib/python3.10/site-packages/cryptography/hazmat/backends/openssl/hmac.py", line 85, in verify raise InvalidSignature("Signature did not match digest.") cryptography.exceptions.InvalidSignature: Signature did not match digest. During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/tkinter/__init__.py", line 1921, in __call__ return self.func(*args) File "/Users//PycharmProjects/pythonProject/password-manager/p_main.py", line 112, in find_password decrypted_password = password_enc()[1].decrypt(password_enc()[0]) File "/Users//PycharmProjects/pythonProject/Day-24/venv/lib/python3.10/site-packages/cryptography/fernet.py", line 90, in decrypt return self._decrypt_data(data, timestamp, time_info) File "/Users//PycharmProjects/pythonProject/Day-24/venv/lib/python3.10/site-packages/cryptography/fernet.py", line 151, in _decrypt_data self._verify_signature(data) File "/Users//PycharmProjects/pythonProject/Day-24/venv/lib/python3.10/site-packages/cryptography/fernet.py", line 135, in _verify_signature raise InvalidToken cryptography.fernet.InvalidToken
问题根源
- 加密逻辑完全错误:
password_enc()每次调用都会生成随机盐值,且用generate_password()生成的随机密码派生密钥,加密的也是随机密码,和用户输入的密码无关。 - 未保存盐值:加密用的盐值没有存入数据库,重启程序后无法还原密钥,导致解密失败。
- 解密逻辑错误:解密时再次调用
password_enc()生成新的密钥,和加密时的密钥完全不匹配,必然触发InvalidToken。
修复方案
1. 更新数据库表结构
添加salt字段存储加密时的随机盐值,执行一次后注释:
cursor.execute('CREATE TABLE UserData (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, ' 'website varchar(250) NOT NULL UNIQUE, ' 'email varchar(250) NOT NULL, ' 'password blob NOT NULL, ' 'salt blob NOT NULL)') db.commit()
2. 重构加解密函数
基于全局主密码派生密钥(所有密码用同一主密码加密,更安全合理):
def derive_key(master_password: str, salt: bytes) -> bytes: # 用主密码和盐值派生Fernet密钥 kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=salt, iterations=480000 ) return base64.urlsafe_b64encode(kdf.derive(master_password.encode())) def encrypt_password(master_password: str, user_password: str) -> tuple[bytes, bytes]: salt = os.urandom(16) key = derive_key(master_password, salt) f = Fernet(key) encrypted = f.encrypt(user_password.encode()) return encrypted, salt def decrypt_password(master_password: str, encrypted_pwd: bytes, salt: bytes) -> str: key = derive_key(master_password, salt) f = Fernet(key) return f.decrypt(encrypted_pwd).decode()
3. 修改save()函数
存储加密后的密码和对应盐值:
# 建议在程序启动时让用户输入主密码,这里示例用全局变量 MASTER_PASSWORD = "" # 实际使用时可通过弹窗设置 def save(): website = website_entry.get().strip() user_email = email_entry.get().strip() user_password = password_entry.get().strip() if not website or not user_password or not MASTER_PASSWORD: messagebox.showinfo(title="错误", message="请填写完整信息并设置主密码") return try: encrypted_pwd, salt = encrypt_password(MASTER_PASSWORD, user_password) cursor.execute( "INSERT INTO UserData (website, email, password, salt) VALUES (?, ?, ?, ?)", (website, user_email, encrypted_pwd, salt) ) db.commit() messagebox.showinfo(title="成功", message="密码已保存") except sqlite3.IntegrityError: messagebox.showinfo(title="错误", message="该网站记录已存在") finally: website_entry.delete(0, tk.END) password_entry.delete(0, tk.END)
4. 修改find_password()函数
从数据库获取盐值,用主密码还原密钥后解密:
def find_password(): website = website_entry.get().strip() if not website or not MASTER_PASSWORD: messagebox.showinfo(title="错误", message="请输入网站名称并设置主密码") return cursor.execute("SELECT email, password, salt FROM UserData WHERE website=?", (website,)) result = cursor.fetchone() if not result: messagebox.showinfo(title="错误", message=f"未找到{website}的记录") return user_email, encrypted_pwd, salt = result try: decrypted_pwd = decrypt_password(MASTER_PASSWORD, encrypted_pwd, salt) messagebox.showinfo(title=website, message=f"邮箱: {user_email}\n密码: {decrypted_pwd}") pyperclip.copy(decrypted_pwd) except cryptography.fernet.InvalidToken: messagebox.showinfo(title="错误", message="主密码错误或数据已损坏")
5. 补充优化
- 在程序启动时添加主密码验证弹窗,避免全局变量硬编码:
def get_master_password(): global MASTER_PASSWORD MASTER_PASSWORD = tk.simpledialog.askstring("验证", "请输入主密码", show='*') if not MASTER_PASSWORD: messagebox.showinfo(title="错误", message="主密码不能为空,程序将退出") root.quit() # 程序启动时调用 root = tk.Tk() get_master_password()
内容的提问来源于stack exchange,提问作者Eduard
相关产品推荐
相关产品推荐

