个人密码管理器

内容分享9小时前发布
0 0 0

随着科技的不断发达,个人社交的不断拓广以及各种社交软件的爆发,导致需要注册各种账号,一多就经常搞忘,所以就想自己做个密码管理器,就自己研究了以下,包含项目概览、已实现代码说明、运行与调试命令、重要安全提醒、以及把项目打包成 Android APK 的方案与操作步骤。


项目概览

目标:实现一个本地密码管理器,支持多用户登录、每个账户仅能看到自己的条目、实现增删改查(名称/账号/密码/备注)、并保证所有数据加密保存在本地(不上传网络)。主要实现技术(当前仓库):
Python 后端:crypto.py(加密/解密)、vault.py(vault 后端:保存/加载/条目操作)CLI:main.py(注册 / 登录 / 交互式命令)图形界面(桌面):gui.py(Tkinter + ttk,美化版,支持条目列表、详情、密码生成与复制并自动清空剪贴板)本地数据目录:data(每个用户有
username.json
,包含 salt、迭代次数、加密的 vault)
效果
个人密码管理器
个人密码管理器


功能速览(已实现)

多用户注册与登录(
register
/
login
)每个用户独立本地 vault,登录仅能读取本用户数据条目操作:添加 / 列表 / 查看 / 更新 / 删除数据本地加密(PBKDF2 + Fernet)桌面 GUI(Tkinter/ttk):
Treeview 列表,右侧详情面板添加/更新对话框带密码生成器与“显示密码”复制密码至剪贴板后 20s 自动清空
项目不会将数据上传网络(除非你自行实现)


仓库内关键文件(说明)

main.py:CLI 入口
子命令:
register <username>

login <username>

gui
交互式命令:
add
/
list
/
view <id>
/
update <id>
/
delete <id>
/
exit

crypto.py:加密工具
PBKDF2 (SHA256) 派生 key -> 用于 Fernet 对称加密函数:
derive_key(password, salt)

encrypt_bytes(data, password, salt)

decrypt_bytes(token, password, salt)

vault.py:Vault 类和用户文件读写

Vault
:内存条目管理(Entry 类),
save()
将加密后的 vault 写入
data/<username>.json
(包含 base64(salt)、iterations、vault)
register_user(username, master_password)

load_vault(username, master_password)

gui.py:Tkinter/ttk GUI(中文化)

LoginFrame
:登录/注册
VaultFrame
:Treeview + 右侧详情 + 按钮(添加/修改/删除/复制)
EntryDialog
:添加/更新对话(密码生成、可视化)
requirements.txt:依赖(示例有
cryptography
,后续若打包安卓需要加
kivy
等)README.md:项目说明与快速启动(已添加)


关键代码片段(便于理解实现逻辑)

派生密钥(来自 crypto.py):


from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
import base64

def derive_key(password: str, salt: bytes, iterations: int = 390000) -> bytes:
    kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32, salt=salt, iterations=iterations)
    key = kdf.derive(password.encode("utf-8"))
    return base64.urlsafe_b64encode(key)

加/解密:


from cryptography.fernet import Fernet

def encrypt_bytes(data: bytes, password: str, salt: bytes) -> bytes:
    key = derive_key(password, salt)
    return Fernet(key).encrypt(data)

def decrypt_bytes(token: bytes, password: str, salt: bytes) -> bytes:
    key = derive_key(password, salt)
    return Fernet(key).decrypt(token)

Vault 保存(核心思想):


payload = {"entries": [e.to_dict() for e in self.entries]}
raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
token = encrypt_bytes(raw, self.master_password, self.salt)
data = {
    "salt": base64.b64encode(self.salt).decode(),
    "iterations": self.iterations,
    "vault": base64.b64encode(token).decode(),
}
with open(user_file, "w") as f:
    json.dump(data, f)

本地运行(桌面/开发环境)

安装依赖:


python3 -m pip install -r requirements.txt

(如果只运行桌面 GUI,requirements.txt 至少要包含
cryptography

语法检查(可选):


python3 -m py_compile main.py mima/crypto.py mima/vault.py mima/gui.py

运行 CLI:


python3 main.py register alice   # 按提示设置主密码
python3 main.py login alice      # 登录进入交互式命令

运行 GUI(桌面):


python3 main.py gui

打包成 Android APK(方案与步骤概要)

注意:我不能在这里直接为你构建 APK,但可以在仓库中添加 Kivy 应用骨架与
buildozer.spec.template
,并指导你本地完成构建。

推荐方案:使用 Kivy + Buildozer(python-for-android)

优点:可以把 Python 代码打包为 APK,能复用大部分逻辑(vault.py、crypto.py)缺点:需 Linux 环境、安装 Android SDK/NDK、Buildozer 和大量依赖,构建耗时且占空间

大致流程:

在 Linux 上安装 Java(OpenJDK)、Android SDK command-line tools、NDK(建议固定版本,如
23.x
)安装 Buildozer:


python3 -m pip install --user buildozer
# 或者使用虚拟环境

在项目目录中创建
buildozer.spec
(我可提供模板)并设置:

requirements = python3,kivy,cryptography,...

android.ndk_path
,
android.sdk_path
指向本地 SDK/NDK(建议手动安装 SDK/NDK 并指定路径)关闭网络权限:确保不添加
android.permission.INTERNET

运行构建:


buildozer -v android debug
# 第一次构建会下载依赖并打包,可能需要很长时间

构建成功后,APK 位于
bin/
(可安装到手机)

我会在仓库中生成下面文件(如你同意):


mima/android_app.py
(Kivy 应用入口,使用中文 UI,复用现有 vault/crypto)
mima/android.kv
(Kivy 布局)
buildozer.spec.template
(示例配置,注释说明如何填写 SDK/NDK 路径)README.md 中新增 “构建 APK” 章节


如何安装 Android SDK(简洁步骤,Ubuntu/Debian 示例)

(该部分也已整理在之前的回答中;此处列出可复制命令)

安装 Java:


sudo apt update
sudo apt install -y openjdk-11-jdk wget unzip

设置 SDK 根目录:


export ANDROID_SDK_ROOT=$HOME/Android/Sdk
mkdir -p "$ANDROID_SDK_ROOT"

下载 command-line tools(替换下载链接)并解压到
$ANDROID_SDK_ROOT/cmdline-tools/latest


sdkmanager
加入 PATH(临时):


export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools:$PATH"

并在
~/.bashrc
中加入永久变量。

使用 sdkmanager 安装组件(示例):


sdkmanager --sdk_root="$ANDROID_SDK_ROOT" "platform-tools" "platforms;android-31" "build-tools;31.0.0" "cmdline-tools;latest" "ndk;23.1.7779620"
yes | sdkmanager --licenses


buildozer.spec
中指定 SDK/NDK 路径,避免 Buildozer 自动下载造成错误。


安全建议(重要)

主密码不存盘:主密码用于派生密钥,请务必牢记,丢失后无法解密数据。salt:为每个用户随机生成并保存在
data/user.json
(以 base64 存储)。文件访问:将 vault 存放在应用私有目录(移动端为内部存储),避免公开同步或不必要的读写权限。最小权限:打包成 APK 时,不要请求网络或文件上传相关权限(除非必要)。副本和备份:对 data 做离线备份(加密拷贝),避免把解密数据或主密码存网盘(除非你理解并接受风险)。剪贴板:复制密码后自动清空(已实现 20s 自动清空),但剪贴板仍有被其他应用读取的风险。


已完成与下一步(由我完成/可为你完成)

已在仓库实现并翻译为中文的桌面 GUI、CLI、加密后端、文件存储逻辑。如果你回复“开始 Android 打包”,我会:
在仓库中生成 Kivy 应用骨架与
buildozer.spec.template
;更新 requirements.txt(加入
kivy
、必要依赖);在 README.md 中写入详细的构建与调试步骤(包括 SDK/NDK 路径示例、常见问题与解决方法)。
我无法在当前环境替你生成 APK,但会给出可直接在你本地运行的一键命令与说明,并在你遇到构建错误时协助排查。


参考命令(复制使用)

安装依赖(开发/桌面):


python3 -m pip install -r requirements.txt

运行桌面 GUI:


python3 main.py gui

构建 Android(在正确安装 SDK/NDK/Buildozer 并配置好
buildozer.spec
后):


# 安装 buildozer(在虚拟环境或用户目录)
python3 -m pip install --user buildozer
# 进入项目目录
buildozer -v android debug
# 安装(若设备已连接)
buildozer android deploy run

参考代码

crypto.py


import base64
import os
from typing import Tuple

from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from cryptography.fernet import Fernet


def derive_key(password: str, salt: bytes, iterations: int = 390000) -> bytes:
    """从明文密码和 salt 派生出适用于 Fernet 的 key(base64 urlsafe 编码)。"""
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=iterations,
        backend=default_backend(),
    )
    key = kdf.derive(password.encode("utf-8"))
    return base64.urlsafe_b64encode(key)


def encrypt_bytes(data: bytes, password: str, salt: bytes) -> bytes:
    """使用派生密钥加密原始 bytes,返回 token bytes。"""
    key = derive_key(password, salt)
    f = Fernet(key)
    return f.encrypt(data)


def decrypt_bytes(token: bytes, password: str, salt: bytes) -> bytes:
    """使用派生密钥解密 token,失败会抛出异常。"""
    key = derive_key(password, salt)
    f = Fernet(key)
    return f.decrypt(token)

gui.py


import tkinter as tk
from tkinter import messagebox, simpledialog
from tkinter import ttk
from typing import Optional
import secrets
import string

from .vault import register_user, load_vault, Vault, VaultError


def _generate_password(length: int = 14) -> str:
    alphabet = string.ascii_letters + string.digits + "!@#$%^&*()-_=+[]{};:,.<>?"
    return ''.join(secrets.choice(alphabet) for _ in range(length))


class LoginFrame(ttk.Frame):
    def __init__(self, master, on_login):
        super().__init__(master, padding=16)
        self.on_login = on_login
        self._build()

    def _build(self):
        self.columnconfigure(1, weight=1)
        ttk.Label(self, text="Username:").grid(row=0, column=0, sticky="e", pady=6)
        self.username = ttk.Entry(self)
        self.username.grid(row=0, column=1, sticky="ew", pady=6)

        ttk.Label(self, text="Master Password:").grid(row=1, column=0, sticky="e", pady=6)
        self.password = ttk.Entry(self, show="*")
        self.password.grid(row=1, column=1, sticky="ew", pady=6)

        btn_frame = ttk.Frame(self)
        btn_frame.grid(row=2, column=0, columnspan=2, pady=12)
        ttk.Button(btn_frame, text="Login", command=self._login).pack(side="left", padx=6)
        ttk.Button(btn_frame, text="Register", command=self._register).pack(side="left", padx=6)

    def _register(self):
        username = self.username.get().strip()
        pwd = self.password.get()
        if not username or not pwd:
            messagebox.showwarning("Input required", "Please enter username and password")
            return
        try:
            register_user(username, pwd)
            messagebox.showinfo("OK", f"User {username} created. You may now login.")
        except VaultError as e:
            messagebox.showerror("Error", str(e))

    def _login(self):
        username = self.username.get().strip()
        pwd = self.password.get()
        if not username or not pwd:
            messagebox.showwarning("Input required", "Please enter username and password")
            return
        try:
            vault = load_vault(username, pwd)
        except VaultError as e:
            messagebox.showerror("Error", str(e))
            return
        self.on_login(vault)


class EntryDialog(tk.Toplevel):
    def __init__(self, master, title: str, initial: Optional[dict] = None):
        super().__init__(master)
        self.title(title)
        self.resizable(False, False)
        self.result = None
        self.initial = initial or {}
        self._build()
        self.transient(master)
        self.grab_set()
        self.wait_window()

    def _build(self):
        frm = ttk.Frame(self, padding=12)
        frm.grid(row=0, column=0)
        ttk.Label(frm, text="Name:").grid(row=0, column=0, sticky="e")
        self.name = ttk.Entry(frm, width=40)
        self.name.grid(row=0, column=1, pady=6)
        self.name.insert(0, self.initial.get('name', ''))

        ttk.Label(frm, text="Account:").grid(row=1, column=0, sticky="e")
        self.account = ttk.Entry(frm, width=40)
        self.account.grid(row=1, column=1, pady=6)
        self.account.insert(0, self.initial.get('account', ''))

        ttk.Label(frm, text="Password:").grid(row=2, column=0, sticky="e")
        pw_row = ttk.Frame(frm)
        pw_row.grid(row=2, column=1, pady=6, sticky="w")
        self.password = ttk.Entry(pw_row, width=30, show='*')
        self.password.pack(side='left')
        self.password.insert(0, self.initial.get('password', ''))
        ttk.Button(pw_row, text="Gen", command=self._gen).pack(side='left', padx=6)
        self.show_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(pw_row, text="Show", variable=self.show_var, command=self._toggle_show).pack(side='left')

        ttk.Label(frm, text="Notes:").grid(row=3, column=0, sticky="ne")
        self.notes = tk.Text(frm, width=40, height=4)
        self.notes.grid(row=3, column=1, pady=6)
        self.notes.insert('1.0', self.initial.get('notes', ''))

        btns = ttk.Frame(frm)
        btns.grid(row=4, column=0, columnspan=2, pady=8)
        ttk.Button(btns, text="OK", command=self._ok).pack(side='left', padx=6)
        ttk.Button(btns, text="Cancel", command=self._cancel).pack(side='left')

    def _gen(self):
        pw = _generate_password()
        self.password.delete(0, 'end')
        self.password.insert(0, pw)

    def _toggle_show(self):
        if self.show_var.get():
            self.password.config(show='')
        else:
            self.password.config(show='*')

    def _ok(self):
        self.result = {
            'name': self.name.get().strip(),
            'account': self.account.get().strip(),
            'password': self.password.get(),
            'notes': self.notes.get('1.0', 'end').strip(),
        }
        self.destroy()

    def _cancel(self):
        self.result = None
        self.destroy()


class VaultFrame(ttk.Frame):
    def __init__(self, master, vault: Vault, on_logout):
        super().__init__(master, padding=8)
        self.vault = vault
        self.on_logout = on_logout
        self._clipboard_after_id = None
        self._build()
        self._refresh_list()

    def _build(self):
        top = ttk.Frame(self)
        top.pack(fill='x', pady=6)
        ttk.Label(top, text=f"User: {self.vault.username}", font=('Segoe UI', 10, 'bold')).pack(side='left')
        ttk.Button(top, text="Logout", command=self._logout).pack(side='right')

        body = ttk.Frame(self)
        body.pack(fill='both', expand=True)
        body.columnconfigure(0, weight=1)
        body.columnconfigure(1, weight=0)

        # Treeview for entries
        cols = ('name', 'account', 'updated')
        self.tree = ttk.Treeview(body, columns=cols, show='headings', selectmode='browse')
        self.tree.heading('name', text='Name')
        self.tree.heading('account', text='Account')
        self.tree.heading('updated', text='Updated')
        self.tree.column('name', width=220)
        self.tree.column('account', width=200)
        self.tree.column('updated', width=140)
        self.tree.bind('<<TreeviewSelect>>', lambda e: self._show_details())
        self.tree.grid(row=0, column=0, sticky='nsew')

        scrollbar = ttk.Scrollbar(body, orient='vertical', command=self.tree.yview)
        scrollbar.grid(row=0, column=1, sticky='ns')
        self.tree.configure(yscrollcommand=scrollbar.set)

        # Right-side detail & action panel
        right = ttk.Frame(self, padding=8)
        right.pack(fill='y', side='right')
        ttk.Label(right, text='Details', font=('Segoe UI', 10, 'bold')).pack(anchor='w')
        self.detail = tk.Text(right, width=40, height=12, state='disabled', wrap='word')
        self.detail.pack(pady=6)

        btns = ttk.Frame(right)
        btns.pack(fill='x')
        ttk.Button(btns, text='Add', command=self._add).pack(side='left', padx=4)
        ttk.Button(btns, text='Update', command=self._update_selected).pack(side='left', padx=4)
        ttk.Button(btns, text='Delete', command=self._delete_selected).pack(side='left', padx=4)
        ttk.Button(btns, text='Copy Password', command=self._copy_password).pack(side='left', padx=4)

    def _refresh_list(self):
        for r in self.tree.get_children():
            self.tree.delete(r)
        for e in self.vault.list_entries():
            self.tree.insert('', 'end', iid=e['id'], values=(e['name'], e['account'], e['updated_at']))

    def _show_details(self):
        sel = self.tree.selection()
        if not sel:
            self._set_detail('')
            return
        eid = sel[0]
        e = self.vault.get_entry(eid)
        if not e:
            self._set_detail('Entry not found')
            return
        txt = f"Name: {e.name}
Account: {e.account}
Password: {e.password}
Notes: {e.notes}
Created: {e.created_at}
Updated: {e.updated_at}"
        self._set_detail(txt)

    def _set_detail(self, text: str):
        self.detail.config(state='normal')
        self.detail.delete('1.0', 'end')
        self.detail.insert('1.0', text)
        self.detail.config(state='disabled')

    def _add(self):
        dlg = EntryDialog(self.master, 'Add Entry')
        if not dlg.result:
            return
        r = dlg.result
        self.vault.add_entry(name=r['name'], account=r['account'], password=r['password'], notes=r['notes'])
        self.vault.save()
        self._refresh_list()

    def _selected_id(self) -> Optional[str]:
        sel = self.tree.selection()
        if not sel:
            return None
        return sel[0]

    def _update_selected(self):
        eid = self._selected_id()
        if not eid:
            messagebox.showinfo('Select', 'Please select an entry')
            return
        e = self.vault.get_entry(eid)
        if not e:
            messagebox.showerror('Error', 'Entry not found')
            return
        init = e.to_dict()
        dlg = EntryDialog(self.master, 'Update Entry', initial=init)
        if not dlg.result:
            return
        r = dlg.result
        self.vault.update_entry(e.id, name=r['name'], account=r['account'], password=r['password'], notes=r['notes'])
        self.vault.save()
        self._refresh_list()

    def _delete_selected(self):
        eid = self._selected_id()
        if not eid:
            messagebox.showinfo('Select', 'Please select an entry')
            return
        if not messagebox.askyesno('Confirm', 'Delete selected entry?'):
            return
        try:
            self.vault.delete_entry(eid)
            self.vault.save()
            self._refresh_list()
            self._set_detail('')
        except VaultError as ex:
            messagebox.showerror('Error', str(ex))

    def _copy_password(self):
        eid = self._selected_id()
        if not eid:
            messagebox.showinfo('Select', 'Please select an entry')
            return
        e = self.vault.get_entry(eid)
        if not e:
            messagebox.showerror('Error', 'Entry not found')
            return
        # copy to clipboard and schedule clear
        try:
            self.master.clipboard_clear()
            self.master.clipboard_append(e.password)
            messagebox.showinfo('Copied', 'Password copied to clipboard (will clear in 20s)')
            # cancel previous clear if any
            if self._clipboard_after_id:
                self.master.after_cancel(self._clipboard_after_id)
            self._clipboard_after_id = self.master.after(20000, self._clear_clipboard)
        except Exception as ex:
            messagebox.showerror('Error', f'Clipboard error: {ex}')

    def _clear_clipboard(self):
        try:
            self.master.clipboard_clear()
        except Exception:
            pass
        self._clipboard_after_id = None

    def _logout(self):
        if self._clipboard_after_id:
            try:
                self.master.after_cancel(self._clipboard_after_id)
            except Exception:
                pass
        self.on_logout()


class MimaGUI(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title('Mima - Password Manager')
        self.geometry('900x520')
        # use ttk theme
        try:
            style = ttk.Style(self)
            style.theme_use('clam')
        except Exception:
            pass
        self.current_frame = None
        self.vault: Optional[Vault] = None
        self.show_login()

    def _clear_frame(self):
        if self.current_frame:
            self.current_frame.destroy()
            self.current_frame = None

    def show_login(self):
        self._clear_frame()
        self.current_frame = LoginFrame(self, on_login=self._on_login)
        self.current_frame.pack(fill='both', expand=True)

    def _on_login(self, vault: Vault):
        self.vault = vault
        self.show_vault()

    def show_vault(self):
        self._clear_frame()
        self.current_frame = VaultFrame(self, self.vault, on_logout=self._on_logout)
        self.current_frame.pack(fill='both', expand=True)

    def _on_logout(self):
        self.vault = None
        self.show_login()


def run():
    app = MimaGUI()
    app.mainloop()

vault.py


import os
import json
import base64
import uuid
from datetime import datetime
from typing import List, Dict, Optional

from .crypto import encrypt_bytes, decrypt_bytes


ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
DATA_DIR = os.path.join(ROOT_DIR, "data")
os.makedirs(DATA_DIR, exist_ok=True)


def _user_file_path(username: str) -> str:
    return os.path.join(DATA_DIR, f"{username}.json")


class VaultError(Exception):
    pass


class Entry:
    def __init__(self, id: str, name: str, account: str, password: str, notes: str = "", created_at: str = None, updated_at: str = None):
        self.id = id
        self.name = name
        self.account = account
        self.password = password
        self.notes = notes
        self.created_at = created_at or datetime.utcnow().isoformat()
        self.updated_at = updated_at or self.created_at

    def to_dict(self) -> Dict:
        return {
            "id": self.id,
            "name": self.name,
            "account": self.account,
            "password": self.password,
            "notes": self.notes,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
        }

    @staticmethod
    def from_dict(d: Dict) -> "Entry":
        return Entry(
            id=d["id"],
            name=d.get("name", ""),
            account=d.get("account", ""),
            password=d.get("password", ""),
            notes=d.get("notes", ""),
            created_at=d.get("created_at"),
            updated_at=d.get("updated_at"),
        )


class Vault:
    def __init__(self, username: str, master_password: str, salt: bytes, iterations: int = 390000):
        self.username = username
        self.master_password = master_password
        self.salt = salt
        self.iterations = iterations
        self.entries: List[Entry] = []

    def list_entries(self) -> List[Dict]:
        return [e.to_dict() for e in self.entries]

    def get_entry(self, entry_id: str) -> Optional[Entry]:
        for e in self.entries:
            if e.id == entry_id:
                return e
        return None

    def add_entry(self, name: str, account: str, password: str, notes: str = "") -> Entry:
        e = Entry(id=str(uuid.uuid4()), name=name, account=account, password=password, notes=notes)
        self.entries.append(e)
        return e

    def update_entry(self, entry_id: str, **fields) -> Entry:
        e = self.get_entry(entry_id)
        if not e:
            raise VaultError("Entry not found")
        for k, v in fields.items():
            if hasattr(e, k) and v is not None:
                setattr(e, k, v)
        e.updated_at = datetime.utcnow().isoformat()
        return e

    def delete_entry(self, entry_id: str) -> None:
        e = self.get_entry(entry_id)
        if not e:
            raise VaultError("Entry not found")
        self.entries = [x for x in self.entries if x.id != entry_id]

    def save(self) -> None:
        payload = {
            "entries": [e.to_dict() for e in self.entries]
        }
        raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
        token = encrypt_bytes(raw, self.master_password, self.salt)
        data = {
            "salt": base64.b64encode(self.salt).decode(),
            "iterations": self.iterations,
            "vault": base64.b64encode(token).decode(),
        }
        with open(_user_file_path(self.username), "w") as f:
            json.dump(data, f)


def register_user(username: str, master_password: str) -> None:
    path = _user_file_path(username)
    if os.path.exists(path):
        raise VaultError("用户已存在")
    salt = os.urandom(16)
    vault = Vault(username=username, master_password=master_password, salt=salt)
    vault.save()


def load_vault(username: str, master_password: str) -> Vault:
    path = _user_file_path(username)
    if not os.path.exists(path):
        raise VaultError("用户不存在")
    with open(path, "r") as f:
        data = json.load(f)
    salt = base64.b64decode(data["salt"])
    iterations = data.get("iterations", 390000)
    token = base64.b64decode(data["vault"])
    try:
        raw = decrypt_bytes(token, master_password, salt)
    except Exception:
        raise VaultError("主密码错误或 vault 损坏")
    payload = json.loads(raw.decode("utf-8"))
    vault = Vault(username=username, master_password=master_password, salt=salt, iterations=iterations)
    entries = payload.get("entries", [])
    vault.entries = [Entry.from_dict(d) for d in entries]
    return vault

main.py


import argparse
import getpass
import sys

from mima.vault import register_user, load_vault, Vault, VaultError


def interactive_loop(vault: Vault):
    print(f"已登录为 {vault.username}。请输入命令:add/list/view/update/delete/exit")
    while True:
        try:
            cmd = input("mima> ").strip()
        except (EOFError, KeyboardInterrupt):
            print()
            break
        if not cmd:
            continue
        parts = cmd.split()
        op = parts[0].lower()
        if op == "exit":
            break
        elif op == "add":
            name = input("名称: ")
            account = input("账号: ")
            password = getpass.getpass("密码: ")
            notes = input("备注 (可选): ")
            e = vault.add_entry(name=name, account=account, password=password, notes=notes)
            vault.save()
            print(f"已添加条目 {e.id}")
        elif op == "list":
            entries = vault.list_entries()
            for e in entries:
                print(f"{e['id']}  {e['name']}  {e['account']}  (更新: {e['updated_at']})")
        elif op == "view":
            if len(parts) < 2:
                print("用法: view <id>")
                continue
            e = vault.get_entry(parts[1])
            if not e:
                print("未找到")
            else:
                print("---")
                print(f"id: {e.id}")
                print(f"名称: {e.name}")
                print(f"账号: {e.account}")
                print(f"密码: {e.password}")
                print(f"备注: {e.notes}")
                print(f"创建时间: {e.created_at}")
                print(f"更新时间: {e.updated_at}")
        elif op == "update":
            if len(parts) < 2:
                print("用法: update <id>")
                continue
            e = vault.get_entry(parts[1])
            if not e:
                print("未找到")
                continue
            name = input(f"名称 [{e.name}]: ") or e.name
            account = input(f"账号 [{e.account}]: ") or e.account
            pwd = getpass.getpass("密码(留空则保留原值): ") or e.password
            notes = input(f"备注 [{e.notes}]: ") or e.notes
            vault.update_entry(e.id, name=name, account=account, password=pwd, notes=notes)
            vault.save()
            print("已更新")
        elif op == "delete":
            if len(parts) < 2:
                print("用法: delete <id>")
                continue
            confirm = input("输入 'yes' 确认删除: ")
            if confirm.lower() != "yes":
                print("已取消")
                continue
            try:
                vault.delete_entry(parts[1])
                vault.save()
                print("已删除")
            except VaultError as ex:
                print("错误:", ex)
        else:
            print("未知命令")


def cmd_register(args):
    username = args.username
    pwd = getpass.getpass("设置主密码: ")
    pwd2 = getpass.getpass("重复主密码: ")
    if pwd != pwd2:
        print("密码不匹配")
        return
    try:
        register_user(username, pwd)
        print(f"用户 {username} 已创建。请妥善保管主密码。")
    except VaultError as e:
        print("错误:", e)


def cmd_login(args):
    username = args.username
    pwd = getpass.getpass("主密码: ")
    try:
        vault = load_vault(username, pwd)
    except VaultError as e:
        print("错误:", e)
        return
    interactive_loop(vault)


def cmd_gui(args):
    # Lazy import of tkinter GUI to avoid pulling it in for CLI-only use
    try:
        from mima.gui import run
    except Exception as e:
        print("启动 GUI 失败:", e)
        return
    run()


def main():
    parser = argparse.ArgumentParser(description="本地简单密码管理器")
    sub = parser.add_subparsers()

    p_reg = sub.add_parser("register", help="创建新用户")
    p_reg.add_argument("username")
    p_reg.set_defaults(func=cmd_register)

    p_login = sub.add_parser("login", help="用户登录")
    p_login.add_argument("username")
    p_login.set_defaults(func=cmd_login)

    p_gui = sub.add_parser("gui", help="启动图形界面")
    p_gui.set_defaults(func=cmd_gui)

    if len(sys.argv) <= 1:
        parser.print_help()
        return
    args = parser.parse_args()
    if hasattr(args, "func"):
        args.func(args)


if __name__ == "__main__":
    main()

运行命令


python3 main.py gui
© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...