Appearance
离线密码
离线密码是由服务端/APP 通过算法生成的一次性或限时密码,门锁无需联网即可验证。
与普通密码的区别
| 特性 | 普通密码 | 离线密码 |
|---|---|---|
| 生成方 | 用户自定义 | 服务端/APP 算法生成 |
| 存储时机 | 添加时存储 | 首次使用时存储 |
| 联网要求 | 添加时需联网 | 生成和使用均无需联网 |
| 用户关联 | 添加时指定 | 生成时已关联,使用时同步 |
工作原理
为什么门锁不知道用户?
离线密码的设计目标是"无需联网"。门锁验证密码时只能解析出类型和有效期,无法得知用户信息(否则密码会变得很长)。用户关联信息存储在服务端,待联网同步时补全。
需求规格
密码类型
| 类型 | 参数 | 长度 | 典型场景 |
|---|---|---|---|
| 永久密码 | 槽位 ID(1-16) | 6 位 | 常住人员 |
| 单次密码 | 截止时间 | 6 位 | 物业帮忙关窗、快递 |
| 限时密码 | 开始时间 + 时长 | 8 位 | 维修工、假期宠物代管、亲人短住 |
| 时段密码 | 开始 + 时长 + 截止时间 | 9 位 | 限定时间段访问 |
| 循环密码 | 周几 + 截止时间 + 有效期 | 10 位 | 清洁工、保安巡逻 |
| 清空码 | 新密钥种子 | 14 位 | 统一清空所有离线密码 |
约束条件
| 项目 | 要求 |
|---|---|
| 字符集 | 纯数字 0-9,无前导零(6 位密码范围 100000-999999) |
| 密码长度 | 6 位起步,按类型递增 |
| 时间覆盖 | 20 年 |
| 验证成本 | MCU 友好(1-2 次 HMAC) |
| 离线时长 | 允许永不联网 |
密钥管理
- 主密钥:设备初始化时生成,16 字节
- 清空码:携带密钥种子生成新密钥
- 清空效果:更换密钥后,所有旧离线密码自动失效
设计方案
时间基准
Epoch = 2025-01-01 00:00:00 UTC (Unix: 1735689600)| 时间单位 | 用途 | 说明 |
|---|---|---|
| 小时 | 单次/限时密码 | 支持高精度场景 |
| 天 | 循环/时段密码 | 节省位数 |
类型编码
使用 3 bit 编码类型,支持 8 种(预留 2 种扩展):
| 值 | 类型 | 密码长度 | 可用 bit |
|---|---|---|---|
| 0 | 永久密码 | 6 位 | ~19.8 |
| 1 | 单次密码 | 6 位 | ~19.8 |
| 2 | 限时密码 | 8 位 | ~26.5 |
| 3 | 循环密码 | 10 位 | ~33.2 |
| 4 | 时段密码 | 9 位 | ~29.7 |
| 5 | 清空码 | 14 位 | ~46.5 |
| 6-7 | 保留 | - | - |
密码结构
┌─────────┬──────────────┬──────────┐
│ 类型 │ 类型特定参数 │ HMAC签名 │
│ 3 bit │ 可变长 │ 剩余位 │
└─────────┴──────────────┴──────────┘各类型详细编码
永久密码(6 位)
用于常住人员,长期有效。
[类型 3bit][槽位 4bit][签名 12bit] = 19 bit
槽位:0-15,对应槽位 1-16槽位机制
门锁最多支持 16 个永久密码,每个槽位对应一个固定密码。删除某槽位后可重新生成该槽位的新密码,新密码与旧密码不同。
单次密码(6 位)
用于临时访问,仅可使用一次。
[类型 3bit][有效时长 8bit][签名 8bit] = 19 bit
有效时长:1-255 小时(约 10 天),0 表示当天有效时间验证
门锁首次验证通过时记录使用时间,后续验证检查是否超过有效时长。"单次"特性通过门锁记录已使用密码实现。
限时密码(8 位)
用于指定时间段内无限次使用。
[类型 3bit][开始日 9bit][时长 4bit][签名 10bit] = 26 bit
开始日:相对 Epoch 天数 mod 512
时长:指数编码,见下表512 天周期机制
开始日使用 9 bit 存储,取模 512 形成约 1.4 年的周期。验证时门锁在当前日期 ±256 天范围内匹配,确保任意时间点都能找到唯一对应的开始日。由于密码最长有效期为 365 天,不会出现周期重叠导致的误匹配。
时长编码表:
| 值 | 时长 | 值 | 时长 |
|---|---|---|---|
| 0 | 1 天 | 8 | 3 周 |
| 1 | 2 天 | 9 | 4 周 |
| 2 | 3 天 | 10 | 6 周 |
| 3 | 4 天 | 11 | 2 月 |
| 4 | 5 天 | 12 | 3 月 |
| 5 | 6 天 | 13 | 6 月 |
| 6 | 1 周 | 14 | 12 月 |
| 7 | 2 周 | 15 | 永久 |
循环密码(10 位)
用于周期性访问,如每周固定时间。
[类型 3bit][周几 7bit][结束时 5bit][有效期 8bit][签名 10bit] = 33 bit
周几:7 bit 位掩码,支持任意组合
结束时:0-23 点,每天 00:00 至该小时结束前有效
有效期:0-255 天,从首次使用开始计算周几位掩码:
| bit | 含义 | 示例组合 |
|---|---|---|
| bit0 | 周一 | 0000001 = 周一 |
| bit1 | 周二 | 0010100 = 周三和周五 |
| bit2 | 周三 | 0011111 = 工作日 |
| bit3 | 周四 | 1100000 = 周末 |
| bit4 | 周五 | 1111111 = 每天 |
| bit5 | 周六 | |
| bit6 | 周日 |
时段密码(9 位)
限时密码 + 每日时段限制。
[类型 3bit][开始日 9bit][时长 4bit][结束时 5bit][签名 8bit] = 29 bit
开始日:相对 Epoch 天数 mod 512
时长:同限时密码编码表
结束时:0-23 点,每天 00:00 至该小时结束前有效清空码(14 位)
输入后清空所有离线密码,更换密钥。
[类型 3bit][密钥种子 33bit][签名 10bit] = 46 bit
密钥种子:33 bit 随机数
新密钥 = SHA256(设备ID || 旧密钥 || 种子)[0:16]均匀分布
为避免密码有明显规律,使用密钥派生的混淆值:
混淆值 = HMAC(密钥, "obfuscate" || 密码长度)[0:n] mod 密码空间
最终密码 = (原始值 + 混淆值) mod 密码空间 + 最小值- 每个门锁、每种长度的混淆值固定
- 验证时先减去混淆值还原原始值
- 成本:初始化时计算一次
验证流程
长度与类型对应关系:
| 长度 | 允许的类型值 |
|---|---|
| 6 位 | 0(永久)、1(单次) |
| 8 位 | 2(限时) |
| 9 位 | 4(时段) |
| 10 位 | 3(循环) |
| 14 位 | 5(清空码) |
验证成本:1 次 HMAC
安全分析
攻击者视角:盲猜 N 位数字,不知道类型和混淆算法。
碰撞概率(基于签名位数,经代码验证):
| 长度 | 密码空间 | 包含类型 | 签名位数 | 碰撞率 |
|---|---|---|---|---|
| 6 位 | 90 万 | 永久、单次 | 8-12 bit | ~0.02% |
| 8 位 | 9000 万 | 限时 | 10 bit | ~0.01% |
| 9 位 | 9 亿 | 时段 | 8 bit | ~0.03% |
| 10 位 | 90 亿 | 循环 | 10 bit | ~0.01% |
| 14 位 | 90 万亿 | 清空码 | 10 bit | ~0.007% |
暴力破解时间估算(门锁 5 次错误锁定 3 分钟,每小时最多 100 次):
| 长度 | 碰撞率 | 预期破解时间 |
|---|---|---|
| 6 位 | ~1/5000 | ~50 小时(2.1 天) |
| 8 位 | ~1/10000 | ~100 小时(4.2 天) |
| 9 位 | ~1/3300 | ~33 小时(1.4 天) |
| 10 位 | ~1/10000 | ~100 小时(4.2 天) |
| 14 位 | ~1/14000 | ~140 小时(5.8 天) |
安全性说明
- 攻击者盲猜:不知道密码长度、类型、混淆算法
- 设备防护:设备端应增加防穷举机制增强安全性
- 时效性:密码过期后自动失效
- 清空码特殊:虽然碰撞率较高,但误触发只会重置密钥,不会开门
测试验证
验证测试(每种类型 10000 次生成并验证):全部通过。
碰撞测试(每种长度随机生成 10 万次密码验证碰撞率):
按照门锁错误锁定时长规则,完成该碰撞需约 1000 小时
| 长度 | 测试次数 | 碰撞次数 | 碰撞率 |
|---|---|---|---|
| 6 位 | 100000 | 21 | 0.021% |
| 8 位 | 100000 | 14 | 0.014% |
| 9 位 | 100000 | 33 | 0.033% |
| 10 位 | 100000 | 9 | 0.009% |
| 14 位 | 100000 | 7 | 0.007% |
测试脚本:offline_password_test.py、offline_password_collision.py
实现算法
算法使用 HMAC-SHA256 进行签名计算,通过密钥派生的混淆值实现均匀分布。
参考实现:offline_password.py
测试样本
以下测试样本可用于验证算法实现的正确性。
密钥:0123456789abcdef0123456789abcdef
永久密码(6位)
| 槽位 | 密码 |
|---|---|
| 1 | 321857 |
| 2 | 325432 |
| 3 | 331720 |
| 4 | 334911 |
| 5 | 338029 |
| 6 | 343342 |
| 7 | 345827 |
| 8 | 351958 |
| 9 | 353717 |
| 10 | 360273 |
单次密码(6位)
| 有效时长(小时) | 密码 |
|---|---|
| 1 | 386222 |
| 2 | 386650 |
| 4 | 387170 |
| 8 | 388088 |
| 12 | 389043 |
| 24 | 392234 |
| 48 | 398399 |
| 72 | 404439 |
| 168 | 429001 |
| 255 | 451456 |
限时密码(8位)
| 开始日 | 时长索引 | 密码 |
|---|---|---|
| 0 | 0 | 11316542 |
| 1 | 1 | 11333910 |
| 7 | 2 | 11433686 |
| 30 | 3 | 11811456 |
| 60 | 4 | 12303746 |
| 90 | 5 | 12796105 |
| 100 | 6 | 12961674 |
| 150 | 7 | 13781728 |
| 200 | 8 | 14601504 |
| 256 | 9 | 15520393 |
循环密码(10位)
| 周几(位掩码) | 结束时 | 有效期(天) | 密码 |
|---|---|---|---|
| 0000001 | 18 | 30 | 4976363810 |
| 0000010 | 20 | 60 | 4985307791 |
| 0000100 | 12 | 90 | 5000018325 |
| 0001000 | 17 | 120 | 5034914271 |
| 0010000 | 19 | 150 | 5102578387 |
| 0100000 | 22 | 180 | 5237612864 |
| 1000000 | 10 | 200 | 5502923388 |
| 0011111 | 18 | 255 | 5228252842 |
| 1100000 | 23 | 100 | 5774664473 |
| 1111111 | 0 | 50 | 6028630226 |
时段密码(9位)
| 开始日 | 时长索引 | 结束时 | 密码 |
|---|---|---|---|
| 0 | 0 | 18 | 808174043 |
| 1 | 1 | 20 | 808313848 |
| 7 | 2 | 12 | 809106328 |
| 30 | 3 | 17 | 812130625 |
| 60 | 4 | 19 | 816071460 |
| 90 | 5 | 22 | 820012613 |
| 100 | 6 | 10 | 821328280 |
| 150 | 7 | 8 | 827889793 |
| 200 | 8 | 23 | 834455198 |
| 256 | 9 | 0 | 841797716 |
清空码(14位)
| 密钥种子 | 密码 |
|---|---|
| 0 | 12415319820349 |
| 1 | 12415319821475 |
| 100 | 12415319922324 |
| 1000 | 12415320843743 |
| 10000 | 12415330060546 |
| 100000 | 12415422220161 |
| 1000000 | 12416343819643 |
| 8589934591 | 21211412841120 |
| 4294967296 | 16813366330675 |
| 46118400291 | 15660096607340 |
生成脚本:offline_password_sample.py
局限性与建议
安全强度权衡
离线密码算法为了保持密码长度在可接受范围内(6-14位纯数字),在签名位数上做出了牺牲,导致理论碰撞率相对较高。
必须的防护措施:
门锁端必须实现高频尝试限时锁定机制,例如:
- 5次错误后锁定3分钟
- 连续多次锁定后延长锁定时间(如10分钟、30分钟)
- 记录异常尝试并在联网时上报
安全警告
离线密码算法的安全性严重依赖门锁端的防暴力破解机制。如果门锁不限制错误次数,6位密码可在短时间内被暴力破解。
时间同步要求
所有时效性密码(单次、限时、循环、时段)依赖门锁的准确时间。
处理建议:
门锁端硬件:
- 使用高精度 RTC 芯片,减少时间漂移
- 每次联网时同步校准时间
- 电池低电量时提醒用户更换,避免断电导致时间丢失
门锁验证时容差:
为应对门锁时间轻微偏移,建议在算法验证通过后,根据密码类型实现时间容差:
- 限时密码/时段密码:在开始日期 ±256 天范围内找到最接近的匹配日,允许 ±1小时 偏移
- 循环密码:检查周几匹配时考虑跨天边界,允许 ±1小时 偏移
- 单次密码:在有效时长基础上延长 1小时
服务端/APP:
- 向用户展示密码的生效时间和过期时间
- 提供密码失效后的重新生成功能
- 提示用户定期让门锁联网校准时间
- 更换电池后提醒用户及时联网同步时间
时间覆盖范围
当前设计的时间基准为 2025-01-01 00:00:00 UTC,支持约 20 年时间范围。
说明:
- 这是为了节省密码位数做出的工程权衡
- 20年时间范围足够覆盖产品全生命周期
- 如需延长,可在固件更新时调整 Epoch 基准点
密码撤销限制
离线密码无法单独撤销,只能等待自然过期或使用清空码全部清空。
建议的黑名单机制:
门锁端和服务端配合实现黑名单功能以应对密码泄露:
- 门锁端:先检查黑名单,再进行算法验证;黑名单中的密码直接拒绝
- 服务端:记录黑名单密码,避免重复生成
永久密码槽位管理
永久密码基于槽位ID(1-16)和密钥生成,相同槽位生成的密码固定不变。
槽位删除与重新生成:
删除某槽位的永久密码后,如需重新使用该槽位:
- 服务端:重新生成该槽位密码时,需通知门锁清除旧密码的黑名单记录
- 门锁端:收到槽位重置指令后,清除该槽位旧密码的黑名单记录
否则新生成的密码与旧密码相同,会被黑名单阻止使用。
与其他密码机制的优先级
离线密码使用纯数字,可能与其他密码机制(如用户自定义密码)生成相同的数字串。
建议的验证优先级:
1. 用户自定义密码(基于数据库存储)
2. 管理员密码
3. 离线密码(算法验证)
4. 其他密码类型原因:
- 离线密码空间大(90万至90万亿),碰撞概率极低但非零
- 基于存储的密码可精确匹配,应优先验证
- 避免用户自定义密码被误识别为离线密码导致的权限混乱
实现建议:
- 门锁端按优先级顺序验证密码
- 一旦某层级验证通过,停止后续验证
- 记录验证通过的密码类型,便于审计
