You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

问题根源

  1. 加密逻辑完全错误:password_enc()每次调用都会生成随机盐值,且用generate_password()生成的随机密码派生密钥,加密的也是随机密码,和用户输入的密码无关。
  2. 未保存盐值:加密用的盐值没有存入数据库,重启程序后无法还原密钥,导致解密失败。
  3. 解密逻辑错误:解密时再次调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 18:50:24