{"id":163,"date":"2026-04-21T12:15:39","date_gmt":"2026-04-21T04:15:39","guid":{"rendered":"https:\/\/gihut.com\/?p=163"},"modified":"2026-04-21T12:19:17","modified_gmt":"2026-04-21T04:19:17","slug":"%e4%b8%80%e4%b8%aa%e7%ae%80%e5%8d%95%e7%9a%84%e6%9c%ac%e5%9c%b0%e5%af%86%e7%a0%81%e5%ad%98%e5%82%a8","status":"publish","type":"post","link":"https:\/\/gihut.com\/163.html","title":{"rendered":"\u4e00\u4e2a\u7b80\u5355\u7684\u672c\u5730\u5bc6\u7801\u5b58\u50a8"},"content":{"rendered":"<p>\u7b2c\u4e00\u6b21\u4f7f\u7528\u65f6\u9700\u8bbe\u7f6e\u4e3b\u5bc6\u7801\uff0c\u6062\u590d\u5bc6\u7801\uff0c\u5bc6\u4fdd\uff0c\u611f\u89c9\u8fd8\u9a6c\u9a6c\u864e\u864e\uff0c\u5206\u4eab\u7ed9\u6709\u9700\u8981\u7684\u670b\u53cb\uff01<\/p>\n<p>\u4ee3\u7801\u5982\u4e0b\uff1a<\/p>\n<pre class=\"line-numbers\"><code class=\"language-php\">import tkinter as tk\r\nfrom tkinter import ttk\r\nimport json\r\nimport os\r\nimport shutil\r\nimport random\r\nimport string\r\nimport uuid\r\nimport base64\r\nimport hashlib\r\nfrom datetime import datetime\r\nfrom cryptography.fernet import Fernet\r\nfrom cryptography.hazmat.primitives import hashes\r\nfrom cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC\r\nimport getpass\r\n\r\n# ====================== \u8def\u5f84\u914d\u7f6e ======================\r\nBASE_DIR = os.path.join(os.path.expanduser(&quot;~&quot;), &quot;.password_manager&quot;)\r\nDATA_DIR = os.path.join(BASE_DIR, &quot;data&quot;)\r\nBACKUP_DIR = os.path.join(BASE_DIR, &quot;backups&quot;)\r\n\r\nfor d in [BASE_DIR, DATA_DIR, BACKUP_DIR]:\r\n    os.makedirs(d, exist_ok=True)\r\n\r\nDATA_FILE = os.path.join(DATA_DIR, &quot;pm_data.enc&quot;)\r\nANSWER_HASH_FILE = os.path.join(DATA_DIR, &quot;answer_hash.txt&quot;)\r\nSECURITY_QUESTION_FILE = os.path.join(DATA_DIR, &quot;security_question.txt&quot;)\r\nSALT_FILE = os.path.join(DATA_DIR, &quot;salt.bin&quot;)\r\nRECOVERY_KEY_FILE = os.path.join(DATA_DIR, &quot;recovery_key.enc&quot;)\r\n\r\n# ====================== \u5bc6\u7801\u5b58\u50a8\u6838\u5fc3 ======================\r\nclass SafePasswordStore:\r\n    def __init__(self):\r\n        self.items = []\r\n        self.key = None  # \u5f53\u524d\u4f7f\u7528\u7684\u5bc6\u94a5\r\n        self.question = &quot;&quot;\r\n        self.answer = &quot;&quot;\r\n        self.recovery_key = None\r\n\r\n    def derive_key(self, pwd):\r\n        &quot;&quot;&quot;\u6d3e\u751f\u5bc6\u94a5\uff08\u4f7f\u7528\u968f\u673asalt\uff09&quot;&quot;&quot;\r\n        if not os.path.exists(SALT_FILE):\r\n            salt = os.urandom(16)\r\n            with open(SALT_FILE, &quot;wb&quot;) as f:\r\n                f.write(salt)\r\n        else:\r\n            with open(SALT_FILE, &quot;rb&quot;) as f:\r\n                salt = f.read()\r\n        \r\n        kdf = PBKDF2HMAC(\r\n            algorithm=hashes.SHA256(),\r\n            length=32,\r\n            salt=salt,\r\n            iterations=480000\r\n        )\r\n        return base64.urlsafe_b64encode(kdf.derive(pwd.encode()))\r\n\r\n    def hash_answer(self, answer):\r\n        &quot;&quot;&quot;\u751f\u6210\u7b54\u6848\u7684SHA256\u54c8\u5e0c\uff08\u4f7f\u7528salt\uff09&quot;&quot;&quot;\r\n        with open(SALT_FILE, &quot;rb&quot;) as f:\r\n            salt = f.read()\r\n        return hashlib.pbkdf2_hmac('sha256', answer.encode('utf-8'), salt, 100000).hex()\r\n\r\n    def save_recovery_key(self, main_password, recovery_password):\r\n        &quot;&quot;&quot;\u4fdd\u5b58\u6062\u590d\u5bc6\u94a5\uff08\u7528\u6062\u590d\u5bc6\u7801\u52a0\u5bc6\u4e3b\u5bc6\u7801\u6d3e\u751f\u7684\u5bc6\u94a5\uff09&quot;&quot;&quot;\r\n        try:\r\n            # \u7528\u4e3b\u5bc6\u7801\u6d3e\u751f\u5bc6\u94a5\uff08key1\uff09\r\n            key1 = self.derive_key(main_password)\r\n            # \u7528\u6062\u590d\u5bc6\u7801\u6d3e\u751f\u5bc6\u94a5\uff08key2\uff09\r\n            key2 = self.derive_key(recovery_password)\r\n            fernet = Fernet(key2)\r\n            enc_key1 = fernet.encrypt(key1)\r\n            \r\n            with open(RECOVERY_KEY_FILE, &quot;wb&quot;) as f:\r\n                f.write(enc_key1)\r\n            return True\r\n        except Exception as e:\r\n            print(f&quot;\u4fdd\u5b58\u6062\u590d\u5bc6\u94a5\u5931\u8d25: {e}&quot;)\r\n            return False\r\n\r\n    def recover_master_key(self, recovery_password):\r\n        &quot;&quot;&quot;\u4f7f\u7528\u6062\u590d\u5bc6\u7801\u6062\u590d\u539f\u59cb\u5bc6\u94a5\uff08\u5373\u4e3b\u5bc6\u7801\u6d3e\u751f\u7684\u5bc6\u94a5\uff09&quot;&quot;&quot;\r\n        try:\r\n            # \u8bfb\u53d6\u52a0\u5bc6\u7684key1\r\n            with open(RECOVERY_KEY_FILE, &quot;rb&quot;) as f:\r\n                enc_key1 = f.read()\r\n            \r\n            # \u4f7f\u7528\u6062\u590d\u5bc6\u7801\u6d3e\u751f\u5bc6\u94a5\r\n            key2 = self.derive_key(recovery_password)\r\n            fernet = Fernet(key2)\r\n            original_key = fernet.decrypt(enc_key1)\r\n            \r\n            return original_key\r\n        except Exception as e:\r\n            print(f&quot;\u6062\u590d\u5bc6\u94a5\u5931\u8d25: {e}&quot;)\r\n            return None\r\n\r\n    def manual_backup(self):\r\n        try:\r\n            if not os.path.exists(DATA_FILE):\r\n                return False\r\n            ts = datetime.now().strftime(&quot;%Y%m%d_%H%M%S&quot;)\r\n            bk_path = os.path.join(BACKUP_DIR, f&quot;pm_data_{ts}.enc&quot;)\r\n            shutil.copyfile(DATA_FILE, bk_path)\r\n            return True\r\n        except:\r\n            return False\r\n\r\n    def save_all(self):\r\n        try:\r\n            data = json.dumps(self.items, ensure_ascii=False).encode(&quot;utf-8&quot;)\r\n            enc = Fernet(self.key).encrypt(data)\r\n            with open(DATA_FILE, &quot;wb&quot;) as f:\r\n                f.write(enc)\r\n        except Exception as e:\r\n            print(f&quot;\u4fdd\u5b58\u5931\u8d25: {e}&quot;)\r\n            pass\r\n\r\n    def load_data(self, pwd):\r\n        try:\r\n            key = self.derive_key(pwd)\r\n            with open(DATA_FILE, &quot;rb&quot;) as f:\r\n                raw = Fernet(key).decrypt(f.read())\r\n            self.items = json.loads(raw.decode(&quot;utf-8&quot;))\r\n            self.key = key  # \u66f4\u65b0\u5f53\u524d\u5bc6\u94a5\r\n            return True\r\n        except Exception as e:\r\n            print(f&quot;\u52a0\u8f7d\u5931\u8d25: {e}&quot;)\r\n            self.items = []\r\n            self.key = None\r\n            return False\r\n\r\n    def create_new_db(self, pwd, question, answer, recovery_pwd=None):\r\n        # \u786e\u4fddsalt\u5b58\u5728\uff08\u9996\u6b21\u521b\u5efa\u65f6\u751f\u6210\uff09\r\n        if not os.path.exists(SALT_FILE):\r\n            salt = os.urandom(16)\r\n            with open(SALT_FILE, &quot;wb&quot;) as f:\r\n                f.write(salt)\r\n        \r\n        # \u751f\u6210\u7b54\u6848\u54c8\u5e0c\r\n        answer_hash = self.hash_answer(answer)\r\n        \r\n        self.question = question.strip()\r\n        self.answer = answer.strip()\r\n        self.key = self.derive_key(pwd)\r\n        self.items = [{\r\n            &quot;type&quot;: &quot;system&quot;,\r\n            &quot;question&quot;: self.question,\r\n            &quot;answer&quot;: self.answer,\r\n            &quot;id&quot;: &quot;system_config&quot;\r\n        }]\r\n        self.save_all()\r\n        \r\n        # \u4fdd\u5b58\u5bc6\u4fdd\u95ee\u9898\u548c\u7b54\u6848\u54c8\u5e0c\r\n        with open(SECURITY_QUESTION_FILE, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:\r\n            f.write(self.question)\r\n        \r\n        with open(ANSWER_HASH_FILE, &quot;w&quot;, encoding=&quot;utf-8&quot;) as f:\r\n            f.write(answer_hash)\r\n        \r\n        # \u4fdd\u5b58\u6062\u590d\u5bc6\u94a5\uff08\u5982\u679c\u63d0\u4f9b\u4e86\u6062\u590d\u5bc6\u7801\uff09\r\n        if recovery_pwd:\r\n            self.save_recovery_key(pwd, recovery_pwd)\r\n\r\n    def verify_answer(self, ans):\r\n        &quot;&quot;&quot;\u9a8c\u8bc1\u7b54\u6848\uff08\u7528\u4e8e\u4e3b\u5bc6\u7801\u89e3\u5bc6\u540e\u7684\u7b54\u6848\u6bd4\u5bf9\uff09&quot;&quot;&quot;\r\n        return ans.strip() == self.answer\r\n\r\n    def verify_answer_hash(self, ans):\r\n        &quot;&quot;&quot;\u9a8c\u8bc1\u7b54\u6848\u54c8\u5e0c\uff08\u7528\u4e8e\u627e\u56de\u5bc6\u7801\u65f6\u9a8c\u8bc1\uff09&quot;&quot;&quot;\r\n        try:\r\n            with open(ANSWER_HASH_FILE, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:\r\n                stored_hash = f.read().strip()\r\n            input_hash = self.hash_answer(ans)\r\n            return input_hash == stored_hash\r\n        except:\r\n            return False\r\n\r\n    def add_item(self, site, user, pwd):\r\n        item_id = str(uuid.uuid4())\r\n        self.items.append({\r\n            &quot;type&quot;: &quot;item&quot;,\r\n            &quot;site&quot;: site,\r\n            &quot;user&quot;: user,\r\n            &quot;pwd&quot;: pwd,\r\n            &quot;id&quot;: item_id\r\n        })\r\n        self.save_all()\r\n\r\n    def update_item(self, item_id, site, user, pwd):\r\n        for it in self.items:\r\n            if it.get(&quot;id&quot;) == item_id and it.get(&quot;type&quot;) == &quot;item&quot;:\r\n                it[&quot;site&quot;] = site\r\n                it[&quot;user&quot;] = user\r\n                it[&quot;pwd&quot;] = pwd\r\n                break\r\n        self.save_all()\r\n\r\n    def delete_item(self, item_id):\r\n        self.items = [it for it in self.items if it.get(&quot;id&quot;) != item_id]\r\n        self.save_all()\r\n\r\n    def generate_pwd(self, length=16):\r\n        &quot;&quot;&quot;\u751f\u6210\u968f\u673a\u5bc6\u7801&quot;&quot;&quot;\r\n        chars = string.ascii_letters + string.digits + &quot;!@#$%^&amp;*&quot;\r\n        return ''.join(random.choice(chars) for _ in range(length))\r\n\r\n# ====================== \u4e3b\u754c\u9762 ======================\r\nclass App(tk.Tk):\r\n    def __init__(self):\r\n        super().__init__()\r\n        self.title(&quot;\u5bc6\u7801\u7ba1\u7406\u5668&quot;)\r\n        self.geometry(&quot;750x550&quot;)\r\n        self.resizable(False, False)\r\n        self.safe = SafePasswordStore()  # \u521d\u59cb\u5316\u5b89\u5168\u5b58\u50a8\r\n        self.style()\r\n        self.show_login()\r\n\r\n    def style(self):\r\n        style = ttk.Style()\r\n        style.configure(&quot;Treeview.Heading&quot;, font=(&quot;\u5fae\u8f6f\u96c5\u9ed1&quot;, 10, &quot;bold&quot;))\r\n\r\n    def clear(self):\r\n        for w in self.winfo_children():\r\n            w.destroy()\r\n\r\n    def create_modal(self, w, h):\r\n        self.update_idletasks()\r\n        win = tk.Toplevel(self)\r\n        win.geometry(f&quot;{w}x{h}&quot;)\r\n        win.resizable(0, 0)\r\n        win.transient(self)\r\n        win.grab_set()\r\n        main_x = self.winfo_x()\r\n        main_y = self.winfo_y()\r\n        main_w = self.winfo_width()\r\n        main_h = self.winfo_height()\r\n        x = main_x + (main_w - w) \/\/ 2\r\n        y = main_y + (main_h - h) \/\/ 2\r\n        win.geometry(f&quot;+{x}+{y}&quot;)\r\n        return win\r\n\r\n    def msg(self, title, txt):\r\n        m = self.create_modal(400, 160)\r\n        m.title(title)\r\n        tk.Label(m, text=txt, wraplength=350).pack(pady=25)\r\n        ttk.Button(m, text=&quot;\u786e\u5b9a&quot;, command=m.destroy).pack()\r\n        m.wait_window()\r\n\r\n    def confirm(self, title, txt):\r\n        res = False\r\n        m = self.create_modal(400, 170)\r\n        m.title(title)\r\n        tk.Label(m, text=txt, wraplength=350).pack(pady=20)\r\n        def ok(): nonlocal res; res=True; m.destroy()\r\n        frm = tk.Frame(m)\r\n        frm.pack(pady=5)\r\n        ttk.Button(frm, text=&quot;\u786e\u5b9a&quot;, command=ok, width=8).grid(row=0, column=0, padx=10)\r\n        ttk.Button(frm, text=&quot;\u53d6\u6d88&quot;, command=m.destroy, width=8).grid(row=0, column=1, padx=10)\r\n        m.wait_window()\r\n        return res\r\n\r\n    def input(self, title, prompt, show=&quot;&quot;):\r\n        val = &quot;&quot;\r\n        m = self.create_modal(400, 190)\r\n        m.title(title)\r\n        tk.Label(m, text=prompt).pack(pady=15)\r\n        v = tk.StringVar()\r\n        ttk.Entry(m, textvariable=v, show=show, width=30).pack(pady=5)\r\n        def ok(): nonlocal val; val=v.get().strip(); m.destroy()\r\n        ttk.Button(m, text=&quot;\u786e\u5b9a&quot;, command=ok).pack(pady=10)\r\n        m.wait_window()\r\n        return val\r\n\r\n    def show_login(self):\r\n        self.clear()\r\n        main = tk.Frame(self, padx=40, pady=60)\r\n        main.pack(expand=True)\r\n        tk.Label(main, text=&quot;\u5bc6\u7801\u7ba1\u7406\u5668&quot;, font=(&quot;\u5fae\u8f6f\u96c5\u9ed1&quot;, 18, &quot;bold&quot;)).pack(pady=12)\r\n        tk.Label(main, text=&quot;\u8bf7\u8f93\u5165\u4e3b\u5bc6\u7801&quot;).pack()\r\n        self.pwd_entry = ttk.Entry(main, show=&quot;*&quot;, width=28, font=(&quot;&quot;, 11))\r\n        self.pwd_entry.pack(pady=8)\r\n        self.pwd_entry.focus()\r\n        self.pwd_entry.bind(&quot;&lt;Return&gt;&quot;, lambda e: self.do_login())\r\n\r\n        frm = tk.Frame(main)\r\n        frm.pack(pady=10)\r\n        ttk.Button(frm, text=&quot;\u767b\u5f55&quot;, width=12, command=self.do_login).grid(row=0, column=0, padx=5)\r\n        ttk.Button(frm, text=&quot;\u5fd8\u8bb0\u5bc6\u7801&quot;, width=12, command=self.find_pwd).grid(row=0, column=1, padx=5)\r\n\r\n        copyright_label = tk.Label(self, text=&quot;&copy; 2025 gihut.com All Rights Reserved&quot;, anchor=&quot;se&quot;, fg=&quot;#888888&quot;, font=(&quot;Arial&quot;, 9))\r\n        copyright_label.place(relx=1.0, rely=1.0, anchor=&quot;se&quot;, x=-10, y=-5)\r\n\r\n    def do_login(self):\r\n        pwd = self.pwd_entry.get().strip()\r\n        if not pwd:\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u8bf7\u8f93\u5165\u4e3b\u5bc6\u7801&quot;)\r\n            return\r\n\r\n        # \u68c0\u67e5\u6570\u636e\u5e93\u6587\u4ef6\u662f\u5426\u5b58\u5728\r\n        if not os.path.exists(DATA_FILE):\r\n            # \u6570\u636e\u5e93\u4e0d\u5b58\u5728\uff0c\u63d0\u793a\u521b\u5efa\u65b0\u6570\u636e\u5e93\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u6570\u636e\u5e93\u4e0d\u5b58\u5728\uff0c\u8bf7\u8bbe\u7f6e\u4e3b\u5bc6\u7801\u521b\u5efa\u65b0\u6570\u636e\u5e93&quot;)\r\n            self.show_create_db()\r\n            return\r\n\r\n        if self.safe.load_data(pwd):\r\n            self.show_main()\r\n        else:\r\n            self.msg(&quot;\u9519\u8bef&quot;, &quot;\u4e3b\u5bc6\u7801\u9519\u8bef\uff01&quot;)\r\n\r\n    def show_create_db(self):\r\n        self.clear()\r\n        main = tk.Frame(self, padx=40, pady=60)\r\n        main.pack(expand=True)\r\n        tk.Label(main, text=&quot;\u521b\u5efa\u65b0\u6570\u636e\u5e93&quot;, font=(&quot;\u5fae\u8f6f\u96c5\u9ed1&quot;, 18, &quot;bold&quot;)).pack(pady=12)\r\n        tk.Label(main, text=&quot;\u8bbe\u7f6e\u4e3b\u5bc6\u7801\u548c\u5bc6\u4fdd\u95ee\u9898&quot;).pack(pady=5)\r\n\r\n        tk.Label(main, text=&quot;\u4e3b\u5bc6\u7801\uff08\u81f3\u5c114\u4f4d\uff09&quot;).pack()\r\n        self.new_pwd_entry = ttk.Entry(main, show=&quot;*&quot;, width=28, font=(&quot;&quot;, 11))\r\n        self.new_pwd_entry.pack(pady=8)\r\n        self.new_pwd_entry.focus()\r\n        self.new_pwd_entry.bind(&quot;&lt;Return&gt;&quot;, lambda e: self.create_db())\r\n\r\n        tk.Label(main, text=&quot;\u6062\u590d\u5bc6\u7801\uff08\u81f3\u5c118\u4f4d\uff0c\u7528\u4e8e\u5fd8\u8bb0\u4e3b\u5bc6\u7801\u65f6\u6062\u590d\uff09&quot;).pack()\r\n        self.recovery_pwd_entry = ttk.Entry(main, show=&quot;*&quot;, width=28, font=(&quot;&quot;, 11))\r\n        self.recovery_pwd_entry.pack(pady=8)\r\n        tk.Label(main, text=&quot;\uff08\u8bf7\u5b89\u5168\u4fdd\u5b58\u6b64\u5bc6\u7801\uff01\uff09&quot;, fg=&quot;red&quot;, font=(&quot;Arial&quot;, 8)).pack()\r\n\r\n        tk.Label(main, text=&quot;\u5bc6\u4fdd\u95ee\u9898\uff08\u4f8b\u5982\uff1a\u60a8\u5c0f\u5b66\u7684\u73ed\u4e3b\u4efb\u540d\u5b57\uff1f\uff09&quot;).pack()\r\n        self.question_entry = ttk.Entry(main, width=28, font=(&quot;&quot;, 11))\r\n        self.question_entry.pack(pady=8)\r\n\r\n        tk.Label(main, text=&quot;\u5bc6\u4fdd\u7b54\u6848&quot;).pack()\r\n        self.answer_entry = ttk.Entry(main, show=&quot;*&quot;, width=28, font=(&quot;&quot;, 11))\r\n        self.answer_entry.pack(pady=8)\r\n\r\n        frm = tk.Frame(main)\r\n        frm.pack(pady=10)\r\n        ttk.Button(frm, text=&quot;\u521b\u5efa&quot;, width=12, command=self.create_db).grid(row=0, column=0, padx=5)\r\n        ttk.Button(frm, text=&quot;\u8fd4\u56de&quot;, width=12, command=self.show_login).grid(row=0, column=1, padx=5)\r\n\r\n    def create_db(self):\r\n        pwd = self.new_pwd_entry.get().strip()\r\n        recovery_pwd = self.recovery_pwd_entry.get().strip()\r\n        question = self.question_entry.get().strip()\r\n        answer = self.answer_entry.get().strip()\r\n\r\n        if not pwd or len(pwd) &lt; 4:\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u4e3b\u5bc6\u7801\u4e0d\u80fd\u4e3a\u7a7a\u4e14\u957f\u5ea6&ge;4&quot;)\r\n            return\r\n        if not question or not answer:\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u5bc6\u4fdd\u95ee\u9898\u548c\u7b54\u6848\u4e0d\u80fd\u4e3a\u7a7a&quot;)\r\n            return\r\n        if recovery_pwd and len(recovery_pwd) &lt; 8:\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u6062\u590d\u5bc6\u7801\u81f3\u5c11\u9700\u89818\u4f4d&quot;)\r\n            return\r\n\r\n        self.safe.create_new_db(pwd, question, answer, recovery_pwd)\r\n        self.show_main()\r\n\r\n    def find_pwd(self):\r\n        if not os.path.exists(DATA_FILE):\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u672a\u521b\u5efa\u5bc6\u7801\u5e93&quot;)\r\n            return\r\n\r\n        if not os.path.exists(SECURITY_QUESTION_FILE):\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u672a\u8bbe\u7f6e\u5bc6\u4fdd\u95ee\u9898\uff0c\u65e0\u6cd5\u627e\u56de\uff01&quot;)\r\n            return\r\n\r\n        # \u68c0\u67e5\u662f\u5426\u8bbe\u7f6e\u4e86\u6062\u590d\u5bc6\u7801\r\n        if not os.path.exists(RECOVERY_KEY_FILE):\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u672a\u8bbe\u7f6e\u6062\u590d\u5bc6\u7801\uff0c\u65e0\u6cd5\u627e\u56de\uff01&quot;)\r\n            return\r\n\r\n        with open(SECURITY_QUESTION_FILE, &quot;r&quot;, encoding=&quot;utf-8&quot;) as f:\r\n            question = f.read().strip()\r\n\r\n        user_answer = self.input(&quot;\u5bc6\u4fdd\u9a8c\u8bc1&quot;, f&quot;\u5bc6\u4fdd\u95ee\u9898\uff1a\\n{question}\\n\\n\u8bf7\u8f93\u5165\u7b54\u6848\uff1a&quot;)\r\n        if not user_answer:\r\n            return\r\n\r\n        if not self.safe.verify_answer_hash(user_answer):\r\n            self.msg(&quot;\u9519\u8bef&quot;, &quot;\u5bc6\u4fdd\u7b54\u6848\u4e0d\u6b63\u786e\uff01&quot;)\r\n            return\r\n\r\n        # \u8be2\u95ee\u6062\u590d\u5bc6\u7801\r\n        recovery_pwd = self.input(&quot;\u6062\u590d\u5bc6\u7801&quot;, &quot;\u8bf7\u8f93\u5165\u6062\u590d\u5bc6\u7801\uff08\u7528\u4e8e\u5fd8\u8bb0\u4e3b\u5bc6\u7801\u65f6\u6062\u590d\uff09&quot;, show=&quot;*&quot;)\r\n        if not recovery_pwd:\r\n            return\r\n\r\n        # \u9a8c\u8bc1\u6062\u590d\u5bc6\u7801\r\n        if not self.safe.recover_master_key(recovery_pwd):\r\n            self.msg(&quot;\u9519\u8bef&quot;, &quot;\u6062\u590d\u5bc6\u7801\u4e0d\u6b63\u786e\uff01&quot;)\r\n            return\r\n\r\n        # \u63d0\u793a\u8bbe\u7f6e\u65b0\u4e3b\u5bc6\u7801\r\n        new_main_pwd = self.input(&quot;\u65b0\u4e3b\u5bc6\u7801&quot;, &quot;\u8bf7\u8f93\u5165\u65b0\u4e3b\u5bc6\u7801\uff08\u81f3\u5c114\u4f4d\uff09&quot;, show=&quot;*&quot;)\r\n        if not new_main_pwd or len(new_main_pwd) &lt; 4:\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u65b0\u4e3b\u5bc6\u7801\u4e0d\u80fd\u4e3a\u7a7a\u4e14\u957f\u5ea6&ge;4&quot;)\r\n            return\r\n\r\n        # \u5173\u952e\u4fee\u590d\uff1a\u8fd9\u91cc\u4e0d\u518d\u4f7f\u7528self.safe.key\uff0c\u800c\u662f\u91cd\u65b0\u521b\u5efaSafePasswordStore\r\n        if self.save_new_master_password(new_main_pwd, recovery_pwd):\r\n            self.msg(&quot;\u6210\u529f&quot;, &quot;\u4e3b\u5bc6\u7801\u5df2\u6210\u529f\u6062\u590d\uff01\u8bf7\u4f7f\u7528\u65b0\u5bc6\u7801\u767b\u5f55\u3002&quot;)\r\n            # \u91cd\u8981\uff1a\u91cd\u7f6eSafePasswordStore\u5bf9\u8c61\uff0c\u907f\u514d\u5bc6\u94a5\u6b8b\u7559\r\n            self.safe = SafePasswordStore()\r\n            self.show_login()\r\n        else:\r\n            self.msg(&quot;\u9519\u8bef&quot;, &quot;\u5bc6\u7801\u6062\u590d\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5\u3002&quot;)\r\n\r\n    def save_new_master_password(self, new_pwd, recovery_pwd):\r\n        &quot;&quot;&quot;\u4fdd\u5b58\u65b0\u4e3b\u5bc6\u7801\uff08\u5173\u952e\u4fee\u590d\uff1a\u91cd\u7f6eSafePasswordStore\u5bf9\u8c61\uff09&quot;&quot;&quot;\r\n        try:\r\n            # \u6062\u590d\u539f\u59cb\u5bc6\u94a5\uff08\u4e3b\u5bc6\u7801\u6d3e\u751f\u7684\u5bc6\u94a5\uff09\r\n            original_key = self.safe.recover_master_key(recovery_pwd)\r\n            if not original_key:\r\n                return False\r\n            \r\n            # \u7528\u65b0\u4e3b\u5bc6\u7801\u6d3e\u751f\u5bc6\u94a5\r\n            new_key = self.safe.derive_key(new_pwd)\r\n            \r\n            # \u91cd\u65b0\u52a0\u5bc6\u6240\u6709\u6570\u636e\r\n            with open(DATA_FILE, &quot;rb&quot;) as f:\r\n                encrypted_data = f.read()\r\n            \r\n            # \u4f7f\u7528\u539f\u59cb\u5bc6\u94a5\u89e3\u5bc6\r\n            decrypted_data = Fernet(original_key).decrypt(encrypted_data)\r\n            \r\n            # \u7528\u65b0\u5bc6\u94a5\u91cd\u65b0\u52a0\u5bc6\r\n            new_encrypted_data = Fernet(new_key).encrypt(decrypted_data)\r\n            \r\n            # \u4fdd\u5b58\u56de\u6587\u4ef6\r\n            with open(DATA_FILE, &quot;wb&quot;) as f:\r\n                f.write(new_encrypted_data)\r\n            \r\n            return True\r\n        except Exception as e:\r\n            print(f&quot;\u4fdd\u5b58\u65b0\u4e3b\u5bc6\u7801\u5931\u8d25: {e}&quot;)\r\n            return False\r\n\r\n    def manual_backup(self):\r\n        if self.safe.manual_backup():\r\n            self.msg(&quot;\u5907\u4efd\u6210\u529f&quot;, &quot;\u5df2\u751f\u6210\u65f6\u95f4\u6233\u5907\u4efd\u6587\u4ef6&quot;)\r\n        else:\r\n            self.msg(&quot;\u5907\u4efd\u5931\u8d25&quot;, &quot;\u8bf7\u91cd\u8bd5&quot;)\r\n\r\n    def show_backup_list(self):\r\n        backups = []\r\n        for f in os.listdir(BACKUP_DIR):\r\n            if f.endswith(&quot;.enc&quot;):\r\n                backups.append(f)\r\n\r\n        backups.sort(key=lambda x: os.path.getmtime(os.path.join(BACKUP_DIR, x)), reverse=True)\r\n\r\n        if not backups:\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u6682\u65e0\u5907\u4efd&quot;)\r\n            return\r\n\r\n        m = self.create_modal(520, 420)\r\n        m.title(&quot;\u5907\u4efd\u5217\u8868\uff08\u6700\u65b0\u5728\u4e0a\uff09&quot;)\r\n\r\n        frame = tk.Frame(m)\r\n        frame.pack(fill=&quot;both&quot;, expand=True, padx=10, pady=5)\r\n        scroll = tk.Scrollbar(frame)\r\n        scroll.pack(side=&quot;right&quot;, fill=&quot;y&quot;)\r\n        listbox = tk.Listbox(frame, yscrollcommand=scroll.set, font=(&quot;\u5fae\u8f6f\u96c5\u9ed1&quot;, 10))\r\n        listbox.pack(fill=&quot;both&quot;, expand=True)\r\n        scroll.config(command=listbox.yview)\r\n\r\n        for b in backups:\r\n            listbox.insert(&quot;end&quot;, b)\r\n\r\n        def restore():\r\n            if not listbox.curselection():\r\n                self.msg(&quot;\u63d0\u793a&quot;, &quot;\u8bf7\u9009\u62e9\u5907\u4efd&quot;)\r\n                return\r\n            fn = listbox.get(listbox.curselection()[0])\r\n            src = os.path.join(BACKUP_DIR, fn)\r\n\r\n            if os.path.exists(DATA_FILE):\r\n                ts = datetime.now().strftime(&quot;%Y%m%d%H%M%S&quot;)\r\n                shutil.copy(DATA_FILE, os.path.join(DATA_DIR, f&quot;\u6062\u590d\u524d_{ts}.enc&quot;))\r\n\r\n            shutil.copy(src, DATA_FILE)\r\n            self.msg(&quot;\u6210\u529f&quot;, &quot;\u6062\u590d\u5b8c\u6210\uff01\u8bf7\u4f7f\u7528\u65b0\u5bc6\u7801\u767b\u5f55\u3002&quot;)\r\n            m.destroy()\r\n            self.show_login()\r\n\r\n        ttk.Button(m, text=&quot;\u2705 \u6062\u590d\u9009\u4e2d&quot;, command=restore).pack(pady=8)\r\n\r\n    def show_main(self):\r\n        self.clear()\r\n        top = tk.Frame(self, pady=10, padx=10)\r\n        top.pack(fill=&quot;x&quot;)\r\n        ttk.Button(top, text=&quot;\u2795 \u6dfb\u52a0&quot;, command=self.add_item).pack(side=&quot;left&quot;, padx=4)\r\n        ttk.Button(top, text=&quot;\u270f\ufe0f \u4fee\u6539&quot;, command=self.edit_item).pack(side=&quot;left&quot;, padx=4)\r\n        ttk.Button(top, text=&quot;\u2796 \u5220\u9664&quot;, command=self.del_item).pack(side=&quot;left&quot;, padx=4)\r\n        ttk.Button(top, text=&quot;\ud83d\udd04 \u5237\u65b0&quot;, command=self.refresh_list).pack(side=&quot;left&quot;, padx=4)\r\n        ttk.Button(top, text=&quot;\ud83d\udcbe \u624b\u52a8\u5907\u4efd&quot;, command=self.manual_backup).pack(side=&quot;left&quot;, padx=4)\r\n        ttk.Button(top, text=&quot;\ud83d\udcc2 \u6062\u590d\u5907\u4efd&quot;, command=self.show_backup_list).pack(side=&quot;left&quot;, padx=4)\r\n        ttk.Button(top, text=&quot;\ud83d\udeaa \u9000\u51fa\u767b\u5f55&quot;, command=self.show_login).pack(side=&quot;right&quot;, padx=4)\r\n\r\n        tree_f = tk.Frame(self, padx=10, pady=5)\r\n        tree_f.pack(fill=&quot;both&quot;, expand=True)\r\n        self.tree = ttk.Treeview(tree_f, columns=(&quot;site&quot;,&quot;user&quot;,&quot;pwd&quot;), show=&quot;headings&quot;, height=18)\r\n        self.tree.heading(&quot;site&quot;, text=&quot;\u5e94\u7528\/\u7f51\u7ad9&quot;)\r\n        self.tree.heading(&quot;user&quot;, text=&quot;\u8d26\u53f7&quot;)\r\n        self.tree.heading(&quot;pwd&quot;, text=&quot;\u5bc6\u7801\uff08\u53cc\u51fb\u590d\u5236\uff09&quot;)\r\n        self.tree.column(&quot;site&quot;, width=220)\r\n        self.tree.column(&quot;user&quot;, width=280)\r\n        self.tree.column(&quot;pwd&quot;, width=200)\r\n        self.tree.pack(fill=&quot;both&quot;, expand=True)\r\n        self.tree.bind(&quot;&lt;Double-1&gt;&quot;, self.copy_pwd)\r\n        self.refresh_list()\r\n\r\n        copyright_label = tk.Label(self, text=&quot;&copy; 2025 gihut.com All Rights Reserved&quot;, anchor=&quot;se&quot;, fg=&quot;#888888&quot;, font=(&quot;Arial&quot;, 9))\r\n        copyright_label.place(relx=1.0, rely=1.0, anchor=&quot;se&quot;, x=-10, y=-5)\r\n\r\n    def refresh_list(self):\r\n        for i in self.tree.get_children():\r\n            self.tree.delete(i)\r\n        for it in self.safe.items:\r\n            if it.get(&quot;type&quot;) == &quot;item&quot;:\r\n                self.tree.insert(&quot;&quot;, &quot;end&quot;, iid=it[&quot;id&quot;], values=(it[&quot;site&quot;], it[&quot;user&quot;], &quot;\u25cf&quot;*8))\r\n\r\n    def copy_pwd(self, event):\r\n        sel = self.tree.selection()\r\n        if not sel:\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u8bf7\u5148\u9009\u4e2d\u4e00\u6761\u8bb0\u5f55&quot;)\r\n            return\r\n        for it in self.safe.items:\r\n            if it[&quot;id&quot;] == sel[0]:\r\n                self.clipboard_clear()\r\n                self.clipboard_append(it[&quot;pwd&quot;])\r\n                self.msg(&quot;\u6210\u529f&quot;, &quot;\u5bc6\u7801\u5df2\u590d\u5236\u5230\u526a\u8d34\u677f&quot;)\r\n                break\r\n\r\n    def del_item(self):\r\n        sel = self.tree.selection()\r\n        if not sel:\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u8bf7\u9009\u62e9\u4e00\u6761\u8bb0\u5f55&quot;)\r\n            return\r\n        if self.input(&quot;\u786e\u8ba4\u5220\u9664&quot;, &quot;\u8f93\u5165\u3010\u5220\u9664\u3011\u786e\u8ba4\uff1a&quot;) != &quot;\u5220\u9664&quot;:\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u5df2\u53d6\u6d88&quot;)\r\n            return\r\n        self.safe.delete_item(sel[0])\r\n        self.refresh_list()\r\n\r\n    def add_item(self):\r\n        m = self.create_modal(430, 260)\r\n        m.title(&quot;\u6dfb\u52a0\u5bc6\u7801&quot;)\r\n        f = tk.Frame(m, padx=20, pady=20)\r\n        f.pack(expand=True, fill=&quot;both&quot;)\r\n\r\n        tk.Label(f, text=&quot;\u5e94\u7528\/\u7f51\u7ad9&quot;).grid(row=0, column=0, sticky=&quot;w&quot;, pady=6)\r\n        e1 = ttk.Entry(f, width=26)\r\n        e1.grid(row=0, column=1, padx=5)\r\n        e1.config(validate=&quot;key&quot;, validatecommand=(f.register(lambda s: len(s) &lt;= 50), &quot;%P&quot;))\r\n\r\n        tk.Label(f, text=&quot;\u8d26\u53f7&quot;).grid(row=1, column=0, sticky=&quot;w&quot;, pady=6)\r\n        e2 = ttk.Entry(f, width=26)\r\n        e2.grid(row=1, column=1, padx=5)\r\n        e2.config(validate=&quot;key&quot;, validatecommand=(f.register(lambda s: len(s) &lt;= 100), &quot;%P&quot;))\r\n\r\n        tk.Label(f, text=&quot;\u5bc6\u7801&quot;).grid(row=2, column=0, sticky=&quot;w&quot;, pady=6)\r\n        e3 = ttk.Entry(f, width=26)\r\n        e3.grid(row=2, column=1, padx=5)\r\n        e3.config(validate=&quot;key&quot;, validatecommand=(f.register(lambda s: len(s) &lt;= 200), &quot;%P&quot;))\r\n\r\n        def gen():\r\n            e3.delete(0, tk.END)\r\n            e3.insert(0, self.safe.generate_pwd(16))\r\n\r\n        ttk.Button(f, text=&quot;\u751f\u6210&quot;, command=gen).grid(row=2, column=2)\r\n\r\n        def save():\r\n            s = e1.get().strip()\r\n            u = e2.get().strip()\r\n            p = e3.get().strip()\r\n            if not s:\r\n                self.msg(&quot;\u63d0\u793a&quot;, &quot;\u5e94\u7528\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a&quot;)\r\n                return\r\n            if len(s) &gt; 50 or len(u) &gt; 100 or len(p) &gt; 200:\r\n                self.msg(&quot;\u63d0\u793a&quot;, &quot;\u8d85\u51fa\u5b57\u7b26\u9650\u5236\uff01&quot;)\r\n                return\r\n            self.safe.add_item(s, u, p)\r\n            self.refresh_list()\r\n            m.destroy()\r\n\r\n        ttk.Button(f, text=&quot;\u4fdd\u5b58&quot;, command=save).grid(row=3, column=0, columnspan=3, pady=12)\r\n\r\n    def edit_item(self):\r\n        sel = self.tree.selection()\r\n        if not sel:\r\n            self.msg(&quot;\u63d0\u793a&quot;, &quot;\u8bf7\u9009\u62e9&quot;)\r\n            return\r\n        item = None\r\n        for it in self.safe.items:\r\n            if it.get(&quot;id&quot;) == sel[0] and it.get(&quot;type&quot;) == &quot;item&quot;:\r\n                item = it\r\n                break\r\n        if not item:\r\n            return\r\n\r\n        m = self.create_modal(430, 260)\r\n        m.title(&quot;\u4fee\u6539\u5bc6\u7801&quot;)\r\n        f = tk.Frame(m, padx=20, pady=20)\r\n        f.pack(expand=True, fill=&quot;both&quot;)\r\n\r\n        tk.Label(f, text=&quot;\u5e94\u7528\u540d\u79f0\uff08\u6700\u591a50\u5b57\uff09&quot;).grid(row=0, column=0, sticky=&quot;w&quot;, pady=6)\r\n        e1 = ttk.Entry(f, width=26)\r\n        e1.grid(row=0, column=1, padx=5)\r\n        e1.config(validate=&quot;key&quot;, validatecommand=(f.register(lambda s: len(s) &lt;= 50), &quot;%P&quot;))\r\n        e1.insert(0, item[&quot;site&quot;])\r\n\r\n        tk.Label(f, text=&quot;\u8d26\u53f7\uff08\u6700\u591a100\u5b57\uff09&quot;).grid(row=1, column=0, sticky=&quot;w&quot;, pady=6)\r\n        e2 = ttk.Entry(f, width=26)\r\n        e2.grid(row=1, column=1, padx=5)\r\n        e2.config(validate=&quot;key&quot;, validatecommand=(f.register(lambda s: len(s) &lt;= 100), &quot;%P&quot;))\r\n        e2.insert(0, item[&quot;user&quot;])\r\n\r\n        tk.Label(f, text=&quot;\u5bc6\u7801\uff08\u6700\u591a200\u5b57\uff09&quot;).grid(row=2, column=0, sticky=&quot;w&quot;, pady=6)\r\n        e3 = ttk.Entry(f, width=26)\r\n        e3.grid(row=2, column=1, padx=5)\r\n        e3.config(validate=&quot;key&quot;, validatecommand=(f.register(lambda s: len(s) &lt;= 200), &quot;%P&quot;))\r\n        e3.insert(0, item[&quot;pwd&quot;])\r\n\r\n        def gen():\r\n            e3.delete(0, tk.END)\r\n            e3.insert(0, self.safe.generate_pwd(16))\r\n\r\n        ttk.Button(f, text=&quot;\u751f\u6210&quot;, command=gen).grid(row=2, column=2)\r\n\r\n        def save():\r\n            s = e1.get().strip()\r\n            u = e2.get().strip()\r\n            p = e3.get().strip()\r\n            if not s:\r\n                self.msg(&quot;\u63d0\u793a&quot;, &quot;\u5e94\u7528\u540d\u79f0\u4e0d\u80fd\u4e3a\u7a7a&quot;)\r\n                return\r\n            if len(s) &gt; 50 or len(u) &gt; 100 or len(p) &gt; 200:\r\n                self.msg(&quot;\u63d0\u793a&quot;, &quot;\u8d85\u51fa\u5b57\u7b26\u9650\u5236\uff01&quot;)\r\n                return\r\n            self.safe.update_item(item[&quot;id&quot;], s, u, p)\r\n            self.refresh_list()\r\n            m.destroy()\r\n\r\n        ttk.Button(f, text=&quot;\u4fdd\u5b58&quot;, command=save).grid(row=3, column=0, columnspan=3, pady=12)\r\n\r\nif __name__ == &quot;__main__&quot;:\r\n    app = App()\r\n    app.mainloop()\r\n\r\n<\/code><\/pre>\n<p><a href=\"http:\/\/gihut.com\/wp-content\/uploads\/2026\/04\/QQ_1776744340613.png\" class=\"lightbox-gallery\" data-lightbox=\"postContentImages\" ><img loading=\"lazy\" decoding=\"async\" src=\"http:\/\/gihut.com\/wp-content\/uploads\/2026\/04\/QQ_1776744340613-323x250.png\" alt=\"\" width=\"323\" height=\"250\" class=\"alignnone size-medium wp-image-165\" srcset=\"https:\/\/gihut.com\/wp-content\/uploads\/2026\/04\/QQ_1776744340613-323x250.png 323w, https:\/\/gihut.com\/wp-content\/uploads\/2026\/04\/QQ_1776744340613-828x640.png 828w, https:\/\/gihut.com\/wp-content\/uploads\/2026\/04\/QQ_1776744340613-768x594.png 768w, https:\/\/gihut.com\/wp-content\/uploads\/2026\/04\/QQ_1776744340613.png 940w\" sizes=\"auto, (max-width: 323px) 100vw, 323px\" \/><\/a><br \/>\n<a href=\"http:\/\/gihut.com\/wp-content\/uploads\/2026\/04\/QQ_1776744266067.png\" class=\"lightbox-gallery\" data-lightbox=\"postContentImages\" ><img loading=\"lazy\" decoding=\"async\" src=\"http:\/\/gihut.com\/wp-content\/uploads\/2026\/04\/QQ_1776744266067-323x250.png\" alt=\"\" width=\"323\" height=\"250\" class=\"alignnone size-medium wp-image-164\" srcset=\"https:\/\/gihut.com\/wp-content\/uploads\/2026\/04\/QQ_1776744266067-323x250.png 323w, https:\/\/gihut.com\/wp-content\/uploads\/2026\/04\/QQ_1776744266067-828x640.png 828w, https:\/\/gihut.com\/wp-content\/uploads\/2026\/04\/QQ_1776744266067-768x594.png 768w, https:\/\/gihut.com\/wp-content\/uploads\/2026\/04\/QQ_1776744266067.png 940w\" sizes=\"auto, (max-width: 323px) 100vw, 323px\" \/><\/a><\/p>\n","protected":false},"excerpt":{"rendered":"\u7b2c\u4e00\u6b21\u4f7f\u7528\u65f6\u9700\u8bbe\u7f6e\u4e3b\u5bc6\u7801\uff0c\u6062\u590d\u5bc6\u7801\uff0c\u5bc6\u4fdd\uff0c\u611f\u89c9\u8fd8\u9a6c\u9a6c\u864e\u864e\uff0c\u5206\u4eab\u7ed9\u6709\u9700\u8981\u7684\u670b\u53cb\uff01 \u4ee3\u7801\u5982\u4e0b\uff1a import tkinter as tk from tkinter import ttk import js","protected":false},"author":1,"featured_media":164,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[],"class_list":["post-163","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-7"],"views":1107,"_links":{"self":[{"href":"https:\/\/gihut.com\/api\/wp\/v2\/posts\/163","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/gihut.com\/api\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/gihut.com\/api\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/gihut.com\/api\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/gihut.com\/api\/wp\/v2\/comments?post=163"}],"version-history":[{"count":2,"href":"https:\/\/gihut.com\/api\/wp\/v2\/posts\/163\/revisions"}],"predecessor-version":[{"id":167,"href":"https:\/\/gihut.com\/api\/wp\/v2\/posts\/163\/revisions\/167"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/gihut.com\/api\/wp\/v2\/media\/164"}],"wp:attachment":[{"href":"https:\/\/gihut.com\/api\/wp\/v2\/media?parent=163"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/gihut.com\/api\/wp\/v2\/categories?post=163"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/gihut.com\/api\/wp\/v2\/tags?post=163"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}