随着科技的不断发达,个人社交的不断拓广以及各种社交软件的爆发,导致需要注册各种账号,一多就经常搞忘,所以就想自己做个密码管理器,就自己研究了以下,包含项目概览、已实现代码说明、运行与调试命令、重要安全提醒、以及把项目打包成 Android APK 的方案与操作步骤。
项目概览
目标:实现一个本地密码管理器,支持多用户登录、每个账户仅能看到自己的条目、实现增删改查(名称/账号/密码/备注)、并保证所有数据加密保存在本地(不上传网络)。主要实现技术(当前仓库):
Python 后端:crypto.py(加密/解密)、vault.py(vault 后端:保存/加载/条目操作)CLI:main.py(注册 / 登录 / 交互式命令)图形界面(桌面):gui.py(Tkinter + ttk,美化版,支持条目列表、详情、密码生成与复制并自动清空剪贴板)本地数据目录:data(每个用户有 ,包含 salt、迭代次数、加密的 vault)
username.json
效果


功能速览(已实现)
多用户注册与登录( /
register)每个用户独立本地 vault,登录仅能读取本用户数据条目操作:添加 / 列表 / 查看 / 更新 / 删除数据本地加密(PBKDF2 + Fernet)桌面 GUI(Tkinter/ttk):
login
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 类和用户文件读写
:内存条目管理(Entry 类),
Vault 将加密后的 vault 写入
save()(包含 base64(salt)、iterations、vault)
data/<username>.json、
register_user(username, master_password)
load_vault(username, master_password)
gui.py:Tkinter/ttk GUI(中文化)
:登录/注册
LoginFrame:Treeview + 右侧详情 + 按钮(添加/修改/删除/复制)
VaultFrame:添加/更新对话(密码生成、可视化)
EntryDialog
requirements.txt:依赖(示例有 ,后续若打包安卓需要加
cryptography 等)README.md:项目说明与快速启动(已添加)
kivy
关键代码片段(便于理解实现逻辑)
派生密钥(来自 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(建议固定版本,如 )安装 Buildozer:
23.x
python3 -m pip install --user buildozer
# 或者使用虚拟环境
在项目目录中创建 (我可提供模板)并设置:
buildozer.spec
requirements = python3,kivy,cryptography,...,
android.ndk_path 指向本地 SDK/NDK(建议手动安装 SDK/NDK 并指定路径)关闭网络权限:确保不添加
android.sdk_path 等
android.permission.INTERNET
运行构建:
buildozer -v android debug
# 第一次构建会下载依赖并打包,可能需要很长时间
构建成功后,APK 位于 (可安装到手机)
bin/
我会在仓库中生成下面文件(如你同意):
(Kivy 应用入口,使用中文 UI,复用现有 vault/crypto)
mima/android_app.py(Kivy 布局)
mima/android.kv(示例配置,注释说明如何填写 SDK/NDK 路径)README.md 中新增 “构建 APK” 章节
buildozer.spec.template
如何安装 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
将 加入 PATH(临时):
sdkmanager
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
在 中指定 SDK/NDK 路径,避免 Buildozer 自动下载造成错误。
buildozer.spec
安全建议(重要)
主密码不存盘:主密码用于派生密钥,请务必牢记,丢失后无法解密数据。salt:为每个用户随机生成并保存在 (以 base64 存储)。文件访问:将 vault 存放在应用私有目录(移动端为内部存储),避免公开同步或不必要的读写权限。最小权限:打包成 APK 时,不要请求网络或文件上传相关权限(除非必要)。副本和备份:对 data 做离线备份(加密拷贝),避免把解密数据或主密码存网盘(除非你理解并接受风险)。剪贴板:复制密码后自动清空(已实现 20s 自动清空),但剪贴板仍有被其他应用读取的风险。
data/user.json
已完成与下一步(由我完成/可为你完成)
已在仓库实现并翻译为中文的桌面 GUI、CLI、加密后端、文件存储逻辑。如果你回复“开始 Android 打包”,我会:
在仓库中生成 Kivy 应用骨架与 ;更新 requirements.txt(加入
buildozer.spec.template、必要依赖);在 README.md 中写入详细的构建与调试步骤(包括 SDK/NDK 路径示例、常见问题与解决方法)。
kivy
我无法在当前环境替你生成 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