首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >从1password到自建密码管理器:2password

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

作者头像
葫芦
发布2026-03-06 18:12:44
发布2026-03-06 18:12:44
2060
举报
文章被收录于专栏:葫芦葫芦

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

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

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

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

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

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

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

二、整体架构设计

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

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

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

三、核心功能模块

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

四、密码添加流程

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

五、数据库设计

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

密码添加/编辑界面:支持账号、密码、网站、备注和 MFA 密钥

六、核心代码实现

下面来看看核心的数据操作代码。数据库连接使用pymysql:

代码语言:javascript
复制
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操作:

代码语言:javascript
复制
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 密码编辑与历史追踪

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

核心实现代码:

代码语言:javascript
复制
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等工具完全兼容:

代码语言:javascript
复制
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批量导入:

代码语言:javascript
复制
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安装包:

代码语言:javascript
复制
# 安装依赖
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"

最终打包结果:

代码语言:javascript
复制
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密钥

明文存储

加密存储

同密码一样需要加密

如果你打算在生产环境使用自建密码管理器,至少应该实现以下安全措施:

代码语言:javascript
复制
# 最基本的密码加密示例
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

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2026-03-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 从1password到自建密码管理器:2password实战指南
    • 一、为什么要自建密码管理器?
    • 二、整体架构设计
    • 三、核心功能模块
    • 四、密码添加流程
    • 五、数据库设计
    • 六、核心代码实现
      • 6.2 密码编辑与历史追踪
    • 七、MFA 实现原理
    • 八、数据导入功能
    • 九、打包与分发
    • 十、安全性讨论
    • 十一、总结
    • 相关资源
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档