Teg

从1password到自建密码管理器:2password

2026/03/04 15:09 21 次阅读 王梓
★ 打赏
✸ ✸ ✸

从1password到自建密码管理器:2password实战指南

作为一个长期使用1password的用户,我被其强大的功能和跨平台体验所折服。然而,每年续费时的心痛感却越来越强烈——尤其是当你只是需要一个安全、可靠的密码管理工具时。2024年,1password的订阅费用又双叒涨价了,于是我决定:自己动手,丰衣足食。

这篇文章将分享我如何从零构建一个功能完善的密码管理器——2password。它不仅满足了我所有的日常需求,还让我对密码管理有了更深的理解。

一、为什么要自建密码管理器?

在开始技术实现之前,让我们先聊聊为什么我决定放弃1password:

  • 成本问题:1password个人版年费约35美元,家庭版更贵。对于个人用户来说,这个价格并不低
  • 功能冗余:我只需要基础的密码存储、MFA支持、分类管理,1password的很多高级功能我从未使用过
  • 数据自主:将所有密码放在第三方云服务,总有一些不安全感(虽然1password安全性确实很好)
  • 学习价值:自己实现一个密码管理器,是非常好的全栈练习项目

于是,我开始规划2password的设计。

二、整体架构设计

2password采用经典的客户端-服务端架构,前端使用PyQt5构建跨平台桌面应用,后端使用MySQL存储数据。整体架构如下:

2password 整体架构图 客户端 (PyQt5 桌面应用) • 密码列表展示 • 添加/编辑密码 • MFA 生成器 • 密码搜索 • 历史记录 pymysql MySQL 数据库服务 127.0.0.1:3306 passwords 表 • id (主键) • account (账号) • password (密码) • website (网站) • notes (备注) • mfa_secret (MFA密钥) history 表 • id (主键) • password_id (外键) • operation (操作类型) • old_value (旧值) • new_value (新值) • created_at (时间戳) 外键关联 本地配置存储: QSettings (PyQt5)

 

先来看看 2password 的实际界面效果:

 

2password 主界面 - 密码列表

 

2password 主界面:密码列表与快捷操作

 

三、核心功能模块

 

2password的功能设计围绕"简洁实用"的原则,主要包含以下模块:

2password 核心功能模块 密码管理 • 添加/编辑/删除密码 • 密码列表展示 • 密码显示/隐藏切换 MFA 生成器 • TOTP 码生成 • 30秒自动刷新 • QR码扫描支持 搜索过滤 • 实时搜索 • 账号/备注筛选 • 快速定位 历史记录 • 操作追踪 • 变更记录 • 不可删除 导入导出 • CSV 批量导入 • 数据迁移 剪贴板 • 一键复制 • 自动清除 安全 • 本地存储 • 数据库加密 数据流: 用户操作 → PyQt5 UI → pymysql → MySQL → 返回渲染

四、密码添加流程

 

让我们通过一个流程图来理解密码添加的完整过程:

密码添加流程图 开始 点击"添加密码"按钮 打开添加对话框 填写密码信息 账号/密码/备注/MFA 验证必填字段 密码字段不能为空 验证通过? 显示错误提示 返回填写 INSERT SQL 写入MySQL数据库 写入历史记录 记录操作类型和时间 完成 UI 刷新流程 1. load_data() 2. SELECT * FROM passwords 3. 解析结果集 4. 更新 self.passwords 5. 更新列表控件 6. 刷新 MFA 显示 7. 启动定时器 8. 显示成功提示 9. 关闭对话框 10. 流程结束

五、数据库设计

 

数据库采用MySQL,设计了两个核心表:passwords(密码表)和history(历史记录表):

数据库 ER 图 passwords (密码表) id INT AUTO_INCREMENT 主键 account VARCHAR(255) 账号 password VARCHAR(255) 密码 (必填) website VARCHAR(500) 网站URL notes TEXT 备注 mfa_secret VARCHAR(255) MFA密钥 history (历史记录表) id INT AUTO_INCREMENT 主键 password_id INT 外键 → passwords.id operation VARCHAR(20) add/edit/delete old_value TEXT 修改前内容(JSON) new_value TEXT 修改后内容(JSON) created_at DATETIME 操作时间 1:N

2password 密码编辑界面

 

密码添加/编辑界面:支持账号、密码、网站、备注和 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 密码编辑与历史追踪

 

密码编辑是比添加更复杂的操作,因为需要同时保存旧值到历史记录,确保所有变更可追溯。核心设计思想是:先记录旧值,再执行更新,最后在同一个事务中写入历史表。

save_edit_password 逻辑流程 1. 读取表单输入 account/password/website/notes/mfa 密码为空? 提示并返回 2. 获取旧密码数据 self.passwords[current_row] 3. UPDATE passwords 更新所有字段 + modified_at 4. INSERT history 记录旧值(密码/账号/备注/MFA) 历史记录保存的字段 password_id -- 关联的密码记录 ID action -- 操作类型("修改") account -- 修改后的账号(新值) old_password -- 修改前的密码(旧值) old_account -- 修改前的账号(旧值) old_website -- 修改前的网站(旧值) old_notes -- 修改前的备注(旧值) old_mfa -- 修改前的MFA密钥(旧值)

核心实现代码:

 

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 表没有提供删除接口,所有操作记录永久保留,这是审计的基本要求

 

2password MFA验证码界面

 

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标准,核心原理是:

TOTP 算法原理 密钥 (Secret) JBSWY3DPEHPK3PXP 时间 (Time) 当前Unix时间戳 步长 (Step) 30秒 HMAC-SHA1(secret, time/step) → 截断 → 6位数字

八、数据导入功能

 

为了方便从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双因素认证
  • 定期备份数据库

 

相关资源

 

✸ ✸ ✸

📜 版权声明

本文作者:王梓 | 原文链接:https://www.bthlt.com/note/369329667-Teg从1password到自建密码管理器:2password

出处:葫芦的运维日志 | 转载请注明出处并保留原文链接

📜 留言板

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