从1password到自建密码管理器:2password实战指南
作为一个长期使用1password的用户,我被其强大的功能和跨平台体验所折服。然而,每年续费时的心痛感却越来越强烈——尤其是当你只是需要一个安全、可靠的密码管理工具时。2024年,1password的订阅费用又双叒涨价了,于是我决定:自己动手,丰衣足食。
这篇文章将分享我如何从零构建一个功能完善的密码管理器——2password。它不仅满足了我所有的日常需求,还让我对密码管理有了更深的理解。
一、为什么要自建密码管理器?
在开始技术实现之前,让我们先聊聊为什么我决定放弃1password:
- 成本问题:1password个人版年费约35美元,家庭版更贵。对于个人用户来说,这个价格并不低
- 功能冗余:我只需要基础的密码存储、MFA支持、分类管理,1password的很多高级功能我从未使用过
- 数据自主:将所有密码放在第三方云服务,总有一些不安全感(虽然1password安全性确实很好)
- 学习价值:自己实现一个密码管理器,是非常好的全栈练习项目
于是,我开始规划2password的设计。
二、整体架构设计
2password采用经典的客户端-服务端架构,前端使用PyQt5构建跨平台桌面应用,后端使用MySQL存储数据。整体架构如下:
先来看看 2password 的实际界面效果:

2password 主界面:密码列表与快捷操作
三、核心功能模块
2password的功能设计围绕"简洁实用"的原则,主要包含以下模块:
四、密码添加流程
让我们通过一个流程图来理解密码添加的完整过程:
五、数据库设计
数据库采用MySQL,设计了两个核心表:passwords(密码表)和history(历史记录表):

密码添加/编辑界面:支持账号、密码、网站、备注和 MFA 密钥
六、核心代码实现
下面来看看核心的数据操作代码。数据库连接使用pymysql:
class ModernPasswordManager(QMainWindow):
def __init__(self):
super().__init__()
# 数据库配置建议从环境变量或配置文件读取
self.db_config = {
'host': os.getenv('DB_HOST', '127.0.0.1'),
'port': int(os.getenv('DB_PORT', 3306)),
'user': os.getenv('DB_USER', 'root'),
'password': os.getenv('DB_PASS', ''),
'database': '2password',
'charset': 'utf8mb4'
}
def get_connection(self):
return pymysql.connect(**self.db_config)
添加密码时的SQL操作:
def add_password(self, account, password, website, notes, mfa_secret):
conn = self.get_connection()
cursor = conn.cursor()
try:
# 插入密码记录
sql = """INSERT INTO passwords
(account, password, website, notes, mfa_secret, created_at, modified_at)
VALUES (%s, %s, %s, %s, %s, NOW(), NOW())"""
cursor.execute(sql, (account, password, website, notes, mfa_secret))
password_id = cursor.lastrowid
# 记录历史
history_sql = """INSERT INTO history
(password_id, operation, new_value, created_at)
VALUES (%s, %s, %s, NOW())"""
cursor.execute(history_sql, (password_id, 'add', json.dumps({
'account': account, 'password': password, 'website': website,
'notes': notes, 'mfa_secret': mfa_secret
})))
conn.commit()
self.load_data()
return True
except Exception as e:
conn.rollback()
print(f"添加失败: {e}")
return False
finally:
cursor.close()
conn.close()
6.2 密码编辑与历史追踪
密码编辑是比添加更复杂的操作,因为需要同时保存旧值到历史记录,确保所有变更可追溯。核心设计思想是:先记录旧值,再执行更新,最后在同一个事务中写入历史表。
核心实现代码:
def save_edit_password(self):
"""编辑保存 - 同时记录旧值到历史表"""
account = self.account_input.text().strip()
password = self.password_input.text().strip()
website = self.website_input.text().strip()
notes = self.notes_input.text().strip()
mfa_secret = self.mfa_input.text().strip()
if not password:
QMessageBox.warning(self, "提示", "密码不能为空")
return
current_row = self.password_list.currentRow()
old_data = self.passwords[current_row] # 获取修改前的完整数据
pwd_id = old_data['id']
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
try:
conn = self.get_connection()
cursor = conn.cursor()
# Step 1: 更新密码记录
cursor.execute("""
UPDATE passwords
SET account=%s, password=%s, website=%s, notes=%s,
mfa_secret=%s, modified_at=%s
WHERE id=%s
""", (account, password, website, notes,
mfa_secret or old_data.get('mfa_secret', ''), now, pwd_id))
# Step 2: 将旧值写入历史表(不可删除,永久保留)
cursor.execute("""
INSERT INTO history
(id, password_id, action, account, website, timestamp,
old_password, old_account, old_website, old_notes, old_mfa_secret)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (f"hist_{datetime.now().strftime('%Y%m%d%H%M%S')}",
pwd_id, "修改", account, website, now,
old_data['password'], old_data['account'],
old_data['website'], old_data['notes'],
old_data.get('mfa_secret', '')))
conn.commit() # 两条SQL在同一事务中提交
cursor.close()
conn.close()
# Step 3: 刷新UI
self.refresh_password_list()
self.refresh_history_list()
self.clear_inputs()
QMessageBox.information(self, "成功", "密码修改成功")
except Exception as e:
QMessageBox.critical(self, "错误", f"修改失败: {str(e)}")
设计要点:
- 事务一致性 -- UPDATE 和 INSERT history 在同一个事务中,要么都成功,要么都回滚
- 旧值完整保留 -- 历史表记录修改前的所有字段(密码、账号、网站、备注、MFA),方便回溯
- MFA 密钥保护 -- 如果编辑时未填写 MFA,保留原有的 MFA 密钥不被清空
- 历史不可删除 -- history 表没有提供删除接口,所有操作记录永久保留,这是审计的基本要求

MFA 验证码生成:30 秒自动刷新,兼容所有 TOTP 应用
七、MFA 实现原理
2password使用pyotp库实现TOTP(基于时间的一次性密码)功能。这与Google Authenticator、1password等工具完全兼容:
import pyotp
def generate_mfa_code(secret):
"""生成6位TOTP验证码"""
totp = pyotp.TOTP(secret)
return totp.now()
def verify_mfa_code(secret, code):
"""验证MFA验证码"""
totp = pyotp.TOTP(secret)
return totp.verify(code)
TOTP算法基于RFC 6238标准,核心原理是:
八、数据导入功能
为了方便从1password或其他密码管理器迁移,2password支持CSV批量导入:
import csv
import pymysql
def import_from_csv(csv_file, default_password="123"):
conn = pymysql.connect(host='127.0.0.1', port=3306,
user='root', password=os.getenv('DB_PASS', ''),
database='2password')
cursor = conn.cursor()
with open(csv_file, 'r', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
# 字段映射: 标题→备注, 用户名→账号
sql = """INSERT INTO passwords
(account, password, website, notes, mfa_secret, created_at, modified_at)
VALUES (%s, %s, %s, %s, %s, NOW(), NOW())"""
cursor.execute(sql, (
row.get('用户名', ''),
default_password, # 默认密码
row.get('URL', ''),
row.get('备注', ''), # 标题作为备注
row.get('mfa', '')
))
conn.commit()
cursor.close()
conn.close()
九、打包与分发
为了方便使用,我使用PyInstaller将应用打包成macOS的.dmg安装包:
# 安装依赖
pip install pyinstaller pymysql pyotp qrcode
# 打包成macOS应用
pyinstaller --name=2password --windowed password_manager.py
# 创建DMG
create-dmg --volname "2password" --window-pos 200 --window-size 600 --icon-size 100 \
--window-center --app-drop-link 600 --icon "2password.app" 200 \
"2password.dmg"
最终打包结果:
2password/
├── src/
│ ├── password_manager.py # 主程序
│ ├── init_db.py # 数据库初始化
│ └── init_db.sql # 表结构
└── dist/
├── 2password.app/ # macOS应用
└── 2password.dmg # 安装包
十、安全性讨论
诚实地说,2password在安全性上与1password有明显差距。这里列出已知的安全局限和改进方向:
| 安全维度 | 2password 现状 | 1password 做法 | 改进方向 |
|---|---|---|---|
| 密码存储 | 明文存储在MySQL中 | AES-256-GCM加密,零知识架构 | 使用cryptography库做AES加密,主密码派生密钥 |
| 传输安全 | 本地连接MySQL,无加密 | TLS加密传输 | 启用MySQL SSL连接,或使用SQLite本地存储 |
| 主密码 | 无主密码保护 | 主密码 + Secret Key双重保护 | 添加应用启动时的主密码验证 |
| 内存安全 | 密码在内存中明文存在 | 使用安全内存,用后清零 | 使用SecureString或mlock锁定内存页 |
| MFA密钥 | 明文存储 | 加密存储 | 同密码一样需要加密 |
如果你打算在生产环境使用自建密码管理器,至少应该实现以下安全措施:
# 最基本的密码加密示例
from cryptography.fernet import Fernet
import base64, hashlib
def derive_key(master_password: str) -> bytes:
"""从主密码派生加密密钥"""
# 使用PBKDF2派生密钥(实际应用中应加salt)
key = hashlib.pbkdf2_hmac('sha256', master_password.encode(), b'salt', 100000)
return base64.urlsafe_b64encode(key)
def encrypt_password(plain_text: str, master_password: str) -> str:
"""加密密码"""
key = derive_key(master_password)
f = Fernet(key)
return f.encrypt(plain_text.encode()).decode()
def decrypt_password(encrypted_text: str, master_password: str) -> str:
"""解密密码"""
key = derive_key(master_password)
f = Fernet(key)
return f.decrypt(encrypted_text.encode()).decode()
这只是最基础的加密方案。真正的安全密码管理器还需要考虑密钥派生函数(Argon2id)、随机盐值、安全内存管理等。2password目前更适合作为学习项目和个人内网使用,不建议在高安全要求的场景下替代1password等成熟方案。
十一、总结
通过这个项目,我不仅省下了每年几十美元的1password订阅费,还学到了很多实用的技术:
- PyQt5 GUI编程:从零学会使用PyQt5构建跨平台桌面应用
- 数据库设计:深入理解关系型数据库的设计与优化
- MFA/TOTP:理解双因素认证的核心原理
- 应用打包:掌握macOS应用的分发方式
更重要的是,这个自建的密码管理器完全满足我的日常需求:
- - 本地存储,数据完全自主可控
- - MFA支持,与所有TOTP应用兼容
- - 历史记录,所有操作可追溯(不可删除)
- - CSV导入,轻松从其他密码管理器迁移
- - 免费使用,再也不用考虑续费问题
如果你也是一个1password的重度用户,不妨考虑自己动手构建一个专属的密码管理器。这不仅是一次技术挑战,更是一种数据自主的态度。
最后提醒:密码安全无小事,无论使用哪种密码管理器,请确保:
- 使用强密码(长度至少12位,包含大小写、数字、特殊字符)
- 为每个网站使用不同的密码
- 启用MFA双因素认证
- 定期备份数据库
相关资源
- TOTP RFC 6238: RFC 6238
- PyQt5 文档: Qt for Python
📜 版权声明
本文作者:王梓 | 原文链接:https://www.bthlt.com/note/369329667-Teg从1password到自建密码管理器:2password
出处:葫芦的运维日志 | 转载请注明出处并保留原文链接


📜 留言板
留言提交后需管理员审核通过才会显示