虽然现在的 App 普遍支持手机号或微信一键登录,但对于开发者来说,GitHub、服务器、各类海外服务的登录依然离不开“邮箱+密码”的传统模式。

我曾经历过一段漫长的“密码管理进化史”:

  1. Excel 时代:最早用一个加密的 Excel 表格记录,在电脑端还凑合。
  2. Flutter App 时代:为了手机查看方便,我曾写过一个简单的 Flutter App,支持指纹查看,但功能简陋,只有新增功能,连“修改”功能都没有。

直到有一次,GitHub 要求我重新登录,由于我频繁重置密码且新旧密码不能重复,导致我彻底记混了。在另一台电脑前尝试了无数次失败后,我意识到:我需要一个足够安全、随时可用、功能完整的个人密码管理工具。

于是,“豆子工具”里的小程序版密码本诞生了。

我的密码本

1. 设计核心:绝对的隐私与自由

在设计之初,我就定下了两个原则:不联网、重加密。

  • 纯离线运行:为了打消用户(包括我自己)对隐私的顾虑,我砍掉了所有联网备份功能。所有的密码数据仅存储在手机本地,备份只能通过聊天文件导出。
  • 三重加密体系:这是我花费心血最多的地方。主密码(Master Password)不存储在设备上。
    • 第一步:用主密码解密 RSA 私钥
    • 第二步:用私钥解密 AES 密钥
    • 第三步:用 AES 密钥解密具体的 JSON 序列化记录
      这种混合加密机制确保了即使手机丢失,只要主密码不泄露,数据依然是安全的。

2. 攻克开发中的“大山”

这个功能的开发过程远比我想象中痛苦。尤其是在小程序环境下处理文件缓存、二进制流转换和加解密逻辑,经常一个 Bug 就要调试好几天。

中途我好几次想过放弃,觉得用 Excel 也可以凑合。但每次想到反复重置密码的痛苦,还是咬牙坚持了下来。为了稳定性,我将原本不稳定的二进制存储改为了 UTF-8 编码的 JSON 序列化,最终实现了丝滑的操作体验,这都是心血的教训。

3. 功能完善:不仅仅是“存一下”

相比之前的 Flutter 版本,这次我补全了所有短板:

  • 增删改查:支持搜索功能,输入标题就能快速定位账号。
  • 备份恢复:加入了完整的备份机制,更换手机时可以轻松迁移数据。
  • 批量操作:支持导入和导出,方便从其他平台平替过来。
  • 密码提示:为了防范“忘记主密码”这个终极灾难,我加入了密码提示功能(建议设一个只有自己懂的暗语)。

4. 字段灵活,满足多样需求

每一条记录都包含了:标题、URL、用户名、密码、备注
无论是一个服务器的 SSH 密码,还是一个冷门网站的登录信息,都能井井有条地分类存放。

5. 核心代码展示

纯小程序本地原生实现,首先是密码本初始化,它会根据用户输入的主密码,生成密码本文件。

// 第一次使用设置方法
    async setupFirstTime(masterPassword, passwordHint) {
        try {
            // 1. 检查是否已有密码本
            if (!passwordManager.hasPasswordBook()) {
                console.log('1. 创建空密码本...');
                const createResult = await passwordManager.createEmptyPasswordBook(masterPassword, passwordHint);
                if (createResult.success) {
                    console.log('✓', createResult.message);
                } else {
                    console.log('创建失败:', createResult.error);
                    wx.showToast({
                        title: createResult.error,
                        icon: 'none'
                    });
                    return;
                }
            }
        } catch (err) {
            console.log('创建密码本错误,', err)
        }
        // 保存主密码安装和提示
        wx.setStorageSync('pwbookInstall', new Date().toLocaleString());
        wx.setStorageSync('passwordHint', passwordHint || '');
        wx.setStorageSync('pwbookBakTime', getSecTs());

        this.setData({
            isFirstTime: false,
            isUnlocked: true,
            showPasswordDialog: false
        });

        wx.showToast({
            title: '初始化成功',
            icon: 'success'
        });

        this.loadPasswordList();
    },

创建密码本文件核心代码:

/**
     * 创建空的密码本(单例限制:只能有一个密码本)
     * @param {string} masterPassword 主密码
     * @param {string} passwordHint 密码提示
     * @returns {Object} 创建结果
     */
    createEmptyPasswordBook(masterPassword, passwordHint = '') {
        try {
            // 检查是否已存在密码本
            if (this.hasPasswordBook()) {
                return {
                    success: false,
                    error: '密码本已存在,每个用户只能有一个密码本'
                };
            }

            console.log('开始创建空密码本...');

            // 验证密码强度
            if (!this.validatePasswordStrength(masterPassword)) {
                throw new Error('主密码必须至少6位');
            }

            // 使用PasswordBookFile创建空密码本
            const result = PasswordBookFile.createEmptyPasswordBook(masterPassword, passwordHint);

            if (result.success) {
                // 初始化内存状态
                this.isUnlocked = true;
                this.passwordList = [];
                this.metadata = {
                    passwordHint: passwordHint,
                    entryCount: 0,
                    createdTime: new Date().toISOString(),
                    lastModified: new Date().toISOString()
                };
                this.masterPassword = masterPassword;

                console.log('✓ 空密码本创建成功');
                return {
                    success: true,
                    message: '空密码本创建成功',
                    passwordHint: passwordHint,
                    entryCount: 0
                };
            } else {
                throw new Error(result.error);
            }

        } catch (error) {
            console.error('创建空密码本失败:', error);
            return {
                success: false,
                error: error.message
            };
        }
    }

文件操作核心代码:

/**
     * 创建空的密码本文件
     * @param {string} masterPassword 主密码
     * @param {string} passwordHint 密码提示
     * @returns {Object} 创建结果
     */
    static createEmptyPasswordBook(masterPassword, passwordHint = '') {
        try {
            console.log('开始创建空密码本文件...');

            // 验证输入参数
            if (!masterPassword || masterPassword.length < 6) {
                throw new Error('主密码必须至少6位');
            }
            // 生成salt
            const salt = KeyManager.generateRandomSalt();
            console.log('✓ Salt密钥生成成功');

            // 生成iv
            const iv = KeyManager.generateRandomIV();
            console.log('✓ Iv密钥生成成功');

            // 生成RSA密钥对
            const keyPair = KeyManager.generateKeyPairWithSalt(masterPassword, salt, iv);
            console.log('✓ RSA密钥对生成成功');
            // 生成AES密钥(用于加密密码列表)
            const aesKey = KeyManager.generateRandomAESKey();
            console.log('✓ AES密钥生成成功');

            // 生成HMAC
            const hmacKey = KeyManager.generateHMACKeyFromPassword(masterPassword, salt);
            console.log('✓ HMAC密钥生成成功');

            // 使用RSA加密AES密钥
            const encryptedAESKey = KeyManager.encryptAESKeyWithRSA(aesKey, keyPair.publicKey);
            console.log('✓ AES密钥加密成功');

            // 创建空的密码列表
            const emptyPasswordList = [];
            const encryptedPasswordList = this.encryptPasswordList(emptyPasswordList, aesKey, iv);

            // 校验内容
            const pwbookData = {
                header: this.createFileHeader(),
                metadata: this.createMetadata(passwordHint),
                keys: {
                    salt: KeyManager.wordArrayToBase64(salt),
                    iv: KeyManager.wordArrayToBase64(iv),
                    publicKey: keyPair.publicKey,
                    encryptedPrivateKey: keyPair.encryptedPrivateKey,
                    encryptedAESKey: encryptedAESKey,
                },
                entries: encryptedPasswordList,
            }
            // 完整性签名
            const integrity = this.calculatePwbookIntegrity(pwbookData, hmacKey);

            // 创建文件数据结构
            const fileData = {
                ...pwbookData,
                integrity: integrity.hash
            };
            // 创建文件
            this.saveToFile(this.FILE_PATH, fileData);

            console.log('✓ 空密码本文件创建成功');
            return {
                success: true,
                message: '空密码本文件创建成功',
                filePath: this.FILE_PATH,
                entryCount: 0,
                passwordHint: passwordHint
            };

        } catch (error) {
            console.error('创建空密码本文件失败:', error);
            return {
                success: false,
                error: error.message
            };
        }
    }

核心的密码本就完成了。有兴趣的朋友可以和我交流。

总结

这个密码本工具是我最用心、也觉得最实用的作品之一。它没有花哨的云同步,却给了我最踏实的安全感。

最后再次提醒: 由于是全离线存储,请务必定期通过“导出备份”功能保存你的数据,并牢记你的主密码。