基于Raspberry Pi Pico的离线硬件密码管理器设计与实现 📅 发布时间:2026/8/19 3:40:44 👁 浏览次数: 1. 项目缘起当密码管理器遇上微控制器如果你和我一样对数据安全有点“强迫症”同时又是个硬件爱好者那你肯定不止一次想过能不能把那些至关重要的密码、密钥从随时可能被入侵的电脑或手机里彻底物理隔离出来市面上成熟的密码管理器固然方便但它们要么依赖云端同步要么在你的主力设备上运行总感觉有那么一丝丝不安全感。几年前我接触到了“Midbar”这个概念它本质上是一个运行在树莓派Raspberry Pi这类微型计算机上的离线密码管理器。这个想法很棒但树莓派 Zero 或 3B 的功耗和体积对于想把它做成一个“钥匙串”大小的随身设备来说还是太大了点。直到我看到了 Raspberry Pi Pico。这块小小的板子搭载着双核 ARM Cortex-M0 微控制器价格低廉功耗极低通过 USB 供电和通信还能直接模拟 USB HID人机接口设备比如键盘。一个念头瞬间击中了我为什么不把 Midbar 的核心功能移植到 Pico 上呢一个完全离线、无需安装任何客户端软件、通过硬件按钮操作、并通过模拟键盘自动输入密码的设备——这听起来就像是终极的物理安全令牌。我把它称为 “Midbar (Raspberry Pi Pico Version)”一个旨在将密码存储与使用场景进行物理硬隔离的探索性项目。这个项目的核心价值在于“极简的物理安全”。它不联网没有无线模块所有数据加密后存储在 Pico 的 Flash 中。使用时通过设备上的几个按钮进行导航和选择确认后Pico 会通过 USB 模拟键盘将对应的用户名和密码“敲”进电脑。这意味着即使你使用的电脑感染了键盘记录器它记录的也只是 Pico 这个“外部键盘”输入的字符流而无法直接窃取到 Pico 内部存储的、经过加密的密码数据库。当然这并非银弹它的安全性建立在硬件本身不被物理破解、主密钥不泄露的前提下。但对于防范常见的软件层面的密码窃取提供了一个非常有趣的思路。2. 核心架构设计在资源受限环境下构建安全堡垒在 Pico 这样的微控制器上实现一个密码管理器与在树莓派或 PC 上开发有本质区别。Pico 没有操作系统内存只有 264KB SRAM存储空间也有限通常 2MB Flash。这意味着我们不能直接套用 SQLite 数据库也无法运行复杂的加密库。一切都需要从零开始精心设计在有限的资源内实现最大的安全性。2.1 存储与加密方案选型首先面临的是数据存储格式。我们需要存储多条记录每条记录至少包含条目名称、用户名、密码、可选的备注和 URL。在 MCU 上一个简单可靠的方法是使用自定义的二进制结构体序列化。typedef struct { uint32_t magic; // 文件标识如 0x4D494442 (MIDB) uint16_t version; // 数据格式版本 uint8_t salt[16]; // 用于派生密钥的盐 uint8_t iv[12]; // 加密用的初始向量 (GCM模式) uint8_t tag[16]; // GCM认证标签 uint32_t num_entries; // 条目数量 // 后面跟着加密后的条目数据块 } db_header_t; typedef struct { uint16_t name_len; uint16_t user_len; uint16_t pass_len; uint16_t note_len; // 变长字段name, user, pass, note 依次存储 } entry_header_t;加密是重中之重。我选择了 AES-256-GCM。AES-256 提供了足够的强度而 GCMGalois/Counter Mode模式同时提供了加密和认证能有效防止密文被篡改。Pico 的 SDK 通过mbedtls库提供了 AES 硬件加速支持这对于在 MCU 上高效执行加密解密至关重要。密钥派生使用 PBKDF2-HMAC-SHA256结合一个随机生成的盐salt将用户输入的主密码Master Password转换成加密用的实际密钥。迭代次数需要权衡安全性与 Pico 的计算速度我最终设定为 100,000 次在 Pico 上解锁大约需要 2-3 秒这是一个可接受的等待时间。注意千万不要将迭代次数设得太低如 1000 次这会大大降低暴力破解的难度。虽然 Pico 计算慢但攻击者可以在强大的 GPU 上快速尝试。2-3 秒的用户等待换来的是攻击者成本指数级增长是值得的。2.2 用户交互与输入设计Pico 没有屏幕这是我们面临的最大挑战。解决方案是使用“模拟键盘输出到电脑屏幕”作为我们的“显示器”。设备上只需要几个物理按钮一个“选择/确认”按钮一个“上/下”滚动按钮或一个五向摇杆或许再加一个“返回/取消”按钮。工作流程如下上电/连接USBPico 被识别为 USB 键盘和串口设备用于调试。输入主密码连接后Pico 自动“敲出”一段预定义的引导字符如提示用户在电脑上的任何文本输入框为了安全最好是一个临时记事本中输入主密码然后按一个特殊的“传输键”如F12。Pico 通过键盘接口“监听”这些输入实际上是通过 USB HID 报告描述符实现的双向通信Pico 可以接收主机发送的按键状态但这比较复杂更简单的方案是让用户输入密码后按一个组合键Pico 检测到该组合键后开始读取紧接着的字符流作为密码。验证与导航主密码验证通过后Pico 开始模拟键盘输入在电脑屏幕上“绘制”一个简单的文本菜单列出所有密码条目。用户通过硬件按钮上下移动光标在屏幕上体现为高亮符号的移动按确认键选择。自动填充选择条目后Pico 会先模拟按下Tab键或鼠标点击事件需要模拟鼠标 HID将焦点切换到目标输入框然后逐字符输入用户名再按Tab键接着输入密码最后模拟按下Enter键。整个过程完全自动化。这里有一个关键技巧为了应对不同网站和应用的登录框顺序差异可以在每条记录中增加一个“填充顺序”或“标签顺序”的配置字段让用户能自定义Tab的次数和焦点切换逻辑。但为了初版简化我们假设标准顺序是“用户名框 - 密码框”。3. 硬件搭建与核心电路解析虽然代码是灵魂但硬件是骨骼。一个可靠且便于交互的硬件载体至关重要。我选择的硬件清单如下Raspberry Pi Pico核心主板。注意要使用支持 USB 连接的版本。OLED 显示屏 (SSD1306, 128x64)这是对原始无屏设计的重大改进。尽管我们可以用电脑屏幕作为输出但在输入主密码等敏感环节在电脑上显示任何反馈都是不安全的。一块小型 I2C OLED 屏可以用于显示菜单、星号密码输入反馈实现完全离机的交互安全性更高。这是强烈推荐的升级。五向导航摇杆模块或多个轻触开关用于菜单导航和确认。五向摇杆上、下、左、右、按下集成度高节省空间。可选蜂鸣器用于操作反馈嘀声。可选LED 指示灯用于显示状态如电源、解锁成功、输入中。洞洞板、导线、外壳用于整合所有部件。电路连接非常简单OLEDVCC接 Pico 的3V3GND接GNDSCL接GP5SDA接GP4。五向摇杆通常输出模拟电压但我们可以将其连接至 Pico 的 ADC 引脚通过读取电压值来判断方向。更简单的方法是使用数字输出的五向开关模块每个方向一个 GPIO。例如上(GP14)、下(GP15)、左(GP16)、右(GP17)、按下(GP18)。蜂鸣器/LED通过一个三极管或 MOSFET 连接至 GPIO 控制避免直接驱动电流过大。供电直接通过 Pico 的 USB 口。整个系统功耗极低甚至可以用充电宝供电。将所有元件焊接在洞洞板上并找一个合适的小盒子如铝制项目盒装起来一个便携的硬件密码管理器原型就诞生了。实操心得在焊接和组装时务必确保 USB 口和按钮/摇杆位置在外壳上开孔准确。第一次打样时我的按钮位置偏差了1毫米导致手感非常别扭。建议先用3D打印或硬纸板制作一个模型确认布局后再进行最终组装。另外在 Pico 的 USB 数据线DP/DM附近走线要小心避免引入噪声导致 USB 枚举不稳定。4. 固件开发详解从存储到自动输入的全链路实现有了硬件我们进入最核心的固件开发环节。我们将使用 Raspberry Pi Pico 的官方 C/C SDK并利用其强大的硬件库。4.1 工程初始化与关键库引入首先需要设置一个 Pico SDK 开发环境。推荐使用pico-sdk和CMake。在CMakeLists.txt中除了链接pico_stdlib等基础库最关键的是添加以下库hardware_flash用于对 Flash 进行读写操作存储我们的加密数据库。tinyusb_device用于实现 USB HID键盘、鼠标功能。这是实现自动输入的核心。mbedtls提供 AES、SHA256、PBKDF2 等加密算法实现。确保在CMakeLists.txt中正确配置find_package(mbedtls)并链接mbedcrypto、mbedx509等库。如果使用 OLED还需要hardware_i2c和对应的 SSD1306 驱动库可以自己写或使用开源实现。主程序框架将包含以下几个核心状态typedef enum { STATE_BOOT, STATE_AWAIT_MASTER_PASSWORD, STATE_VERIFY_PASSWORD, STATE_MAIN_MENU, STATE_ENTRY_SELECTED, STATE_TYPE_USERNAME, STATE_TYPE_PASSWORD, STATE_ERROR } app_state_t;状态机驱动整个应用流程每个状态处理特定的按钮事件、显示更新和 USB 通信。4.2 加密数据库的读写实现数据库操作集中在几个核心函数db_create(),db_unlock(),db_add_entry(),db_find_entry(),db_save()。创建与解锁流程db_create(master_pass)生成随机盐和 IV用 PBKDF2 派生密钥初始化一个空的加密数据块只包含头部和空条目列表用 AES-GCM 加密后写入 Flash 的特定扇区例如从0x10100000开始避开程序存储区。db_unlock(master_pass)从 Flash 读取数据库头部提取盐用同样的 PBKDF2 参数和输入的主密码派生密钥。然后用该密钥尝试解密数据部分并验证 GCM Tag。如果验证失败返回错误成功则解压条目列表到内存中的结构体数组供后续操作。添加条目int db_add_entry(const char *name, const char *user, const char *pass, const char *note) { // 1. 检查内存中条目数组是否已满 // 2. 将新条目添加到内存数组 // 3. 调用 db_save() 将整个内存数组重新加密并写回Flash }db_save()函数是关键它需要将内存中的条目列表序列化成连续的字节流。生成一个新的随机 IV每次保存都应更换 IV。用当前密钥和新的 IV 加密序列化后的数据。计算 GCM Tag。将新的头部含新 IV 和 Tag和加密数据一起写入 Flash。重要警告RP2040 的 Flash 擦写寿命约为 10 万次。频繁保存比如每添加一个条目就保存会快速损耗 Flash。一个优化策略是在内存中维护一个“脏”标志用户完成一系列操作后比如添加了3个条目再提示用户“是否保存更改”然后一次性写入。或者使用磨损均衡算法但这对初期项目过于复杂。最简单的建议是告知用户不要过于频繁地添加/删除条目。4.3 USB HID 键盘模拟与自动输入逻辑使用 TinyUSB 库可以相对轻松地实现 USB HID 设备。我们需要定义一个键盘报告描述符并实现tud_hid_report_complete_cb等回调函数。自动输入函数是项目的“高光”部分。它的目标是将一个字符串以模拟真实键盘输入的方式发送出去。void type_string(const char *str) { for (int i 0; str[i] ! \0; i) { char c str[i]; // 处理特殊字符例如大写字母需要先按下 SHIFT if (c A c Z) { send_hid_keycode(HID_KEY_A (c - A), KEYBOARD_MODIFIER_LEFTSHIFT); } else if (c a c z) { send_hid_keycode(HID_KEY_A (c - a), 0); } else if (c 0 c 9) { send_hid_keycode(HID_KEY_0 (c - 0), 0); } else { // 处理符号如 , _, . // 这需要映射到具体的 HID 键值可能涉及 Shift uint8_t keycode, modifier 0; map_char_to_hid(c, keycode, modifier); send_hid_keycode(keycode, modifier); } sleep_ms(20); // 关键延迟模拟人的输入速度太快会被某些系统忽略或视为异常。 send_hid_keycode(0, 0); // 释放按键 sleep_ms(10); } }send_hid_keycode函数会调用tud_hid_keyboard_report来发送一个 HID 键盘报告。对于Tab和Enter键直接发送对应的HID_KEY_TAB和HID_KEY_ENTER。这里有一个巨大的坑不同操作系统的键盘布局和输入法状态。我们发送的 HID 键码是物理键位而不是字符。例如我们发送HID_KEY_2在美式键盘上输出是2但在按住 Shift 时是。然而如果用户系统是法语布局同一个键码可能对应不同的字符。这是硬件模拟键盘的固有局限。因此这个项目默认假设主机操作系统使用美式键盘布局。在项目文档中必须明确强调这一点。4.4 OLED 显示与按钮交互集成有了 OLED交互体验提升巨大。我们需要一个简单的 UI 层ui_draw_menu()显示条目列表高亮当前选中项。ui_draw_password_input()显示*****用于主密码输入。ui_draw_message()显示状态信息如“解锁中...”、“错误密码不对”。按钮扫描在main循环中完成。对于五向摇杆通过 ADC 读取或 GPIO 电平检测判断方向。状态机根据当前状态和按钮事件进行跳转。例如在STATE_MAIN_MENU状态下按下“上”按钮就更新当前选中索引并调用ui_draw_menu()刷新屏幕。将显示、输入、加密、USB 输出这几个模块通过状态机有机结合起来就构成了完整的应用逻辑。调试阶段可以同时启用 USB 串口 (stdio_init_all())通过printf打印内部状态极大方便了开发。5. 安全考量、局限性与进阶优化在为一个安全设备沾沾自喜之前我们必须冷静地审视其攻击面和局限性。5.1 潜在攻击面分析物理攻击Flash 提取攻击者可以拆焊 Flash 芯片用编程器直接读取内容。我们依赖加密来防御。只要主密码足够强AES-256 目前是安全的。侧信道攻击通过分析设备运行时的功耗、电磁辐射来推测密钥。这对于消费级攻击不现实但理论上存在。使用带有硬件 AES 加速且对侧信道攻击有防护的芯片会更安全Pico 的 RP2040 在这方面并非专为安全设计。调试接口RP2040 的 SWD 调试接口如果未禁用可能被用来读取内存。在生产版本中应在烧录后锁定 Flash 和禁用调试接口。逻辑/软件攻击暴力破解主密码发生在主机电脑上。我们通过 PBKDF2 高迭代次数增加成本。中间人攻击在 Pico 模拟键盘输入时主机上的恶意软件可以拦截键盘输入。这无法防御这也是硬件令牌的通用风险。我们的价值在于数据库本身不被窃取。固件篡改攻击者能否刷入恶意固件来窃取主密码可以如果设备物理上被获取且没有启动验证。我们可以考虑实现简单的启动签名验证但 Pico 本身不支持安全启动这是一个短板。5.2 项目的核心局限性单点故障设备丢失或损坏且没有备份所有密码将永久丢失。必须强调用户需要定期备份加密数据库文件可以通过一个特殊的“备份模式”将加密的二进制数据以文件形式输出到电脑。无自动同步密码修改后需要在电脑上手动修改再在设备上更新记录繁琐易出错。键盘布局依赖如前所述严重依赖美式键盘布局。输入框定位自动Tab切换不一定适用于所有登录页面。复杂的登录表单如验证码、多因素输入会使其失效。RP2040 非安全芯片它是一款通用的高性能 MCU并非像 ATECC608A 那样的安全元件无法提供防物理探测、安全存储密钥等最高等级的保护。5.3 可行的进阶优化方向尽管有局限但项目有丰富的优化空间备份与恢复实现一个通过 USB 串口或虚拟磁盘Mass Storage的备份/恢复协议。备份时将整个加密的数据库文件复制出来恢复时将文件写回。FIDO2/WebAuthn 集成这是更激动人心的方向。Pico 能否作为一个简单的 FIDO2 安全密钥部分实现是可能的可以用于网站的双因素认证但这需要实现复杂的 CTAP 协议工作量巨大。蓝牙支持增加一个蓝牙模块如 HC-05 或 ESP32让手机也能通过特定 App 与设备通信管理密码。但这会引入无线攻击面与“物理隔离”的初衷相悖需谨慎权衡。使用安全芯片外挂一颗 ATECC608A 芯片用于安全地存储加密密钥的主密钥。这样即使 Flash 被提取攻击者没有安全芯片里的密钥也无法解密。主密码仅用于解锁对安全芯片的访问而非直接用于解密数据。改进 UI使用更小的彩色 LCD 屏支持图标和更美观的界面。开发 Midbar (Pico Version) 的过程是一次在资源、成本和安全性之间寻找平衡的实践。它不是一个可以替代 1Password 或 KeePass 的商业产品而是一个极客的安全玩具一个理解硬件安全基础原理的绝佳平台。它让你真切地感受到每一个比特的数据是如何被加密、存储和使用的以及为了保障其安全需要在设计和实现上付出怎样的心思。最终当我拿着这个自己焊接的小盒子按下按钮看着它自动在登录框里填满复杂的密码时那种对数据和流程的完全掌控感是任何软件都无法给予的。