
滚动代码是什么:从基础原理到常见用法
遥控车库门时,如果每次按键发出的都是同一串无线信号,旁人只要录下信号,就可能在之后重放它来开门。滚动代码(rolling code),也常称为跳码(hopping code),正是为了解决这类重放问题而设计的一种机制:发送端每次使用不同的代码,接收端验证代码是否有效,并拒绝已经使用过的旧代码。
它常见于车库门遥控器、汽车遥控钥匙、无线门禁和部分低功耗遥控设备。需要注意,滚动代码不是某一种固定算法,也不等同于加密。一个可靠的系统通常还需要密钥、消息认证、计数器管理和安全的配对流程。本文会从原理讲到适用场景,并用一段可运行的 Python 示例说明基本设计;示例用于理解机制,不是可直接投入产品的无线协议。
滚动代码解决什么问题
无线遥控器发送的信号可能被附近设备接收。若信号长期不变,攻击者只需记录一次,就能尝试重复发送,这称为重放攻击。对于开门、解锁等操作,重复执行一条已捕获命令可能造成实际风险。
固定代码遥控器的核心缺陷是“有效信号不变”。滚动代码让每次发送的认证数据发生变化,并让接收端记住已经接受到的位置。即使攻击者捕获了一条合法消息,之后重放同一条消息时,接收端也能发现它已经过期或使用过。
“每次都变”本身并不够。如果代码只是按顺序递增,且没有认证保护,旁人可能推测后续值,或者伪造计数器。因此,滚动代码通常把一个不断变化的计数器与共享密钥结合,通过密码学算法生成认证标签。接收端验证标签,并检查计数器是否符合预期。
基本工作原理
一个简化的滚动代码系统包含发送端、接收端和配对时建立的共享密钥。发送端维护计数器 counter,每发送一次有效操作就递增;它根据密钥、计数器和命令生成认证标签。接收端收到消息后,先确认标签正确,再判断计数器是否比已接受的位置更新。两项都通过,才执行开门等操作,并保存新的计数器。
- 配对:发送端和接收端建立彼此认可的密钥或设备身份。配对流程是否安全,直接影响后续保护。
- 生成消息:发送端读取计数器,构造消息,并使用密钥生成消息认证码(MAC)。
- 发送:通过无线链路发送计数器、命令及认证标签。无线链路可能被监听,也可能丢包。
- 验证:接收端检查消息认证码,确认内容由持有正确密钥的一方生成,且未被修改。
- 检查新旧:接收端验证计数器符合允许的前进范围,并在接受后更新持久化状态。
计数器通常不是秘密。即使攻击者能看见计数器,只要认证标签无法在没有密钥的情况下伪造,并且接收端严格拒绝旧计数器,重放和简单伪造仍可被阻止。保密性与真实性是不同目标:滚动代码主要解决消息真实性和防重放,消息内容是否隐藏则要另行设计。
同步窗口:丢包与防重放之间的折中
发送端每按一次键,计数器就可能前进一步。但无线信号会丢失:用户连续按了几次,接收端也许只收到最后一条。如果接收端只接受恰好比当前计数器大一的消息,发送端与接收端就可能失去同步。
常见做法是设置一个有限的前向窗口。例如,接收端当前保存计数器 100,允许在 101 到 132 范围内寻找一条认证有效的消息。若验证通过,就把接收端状态更新到该消息对应的计数器。窗口能容忍一定数量的丢包,但窗口越大,允许尝试的候选状态越多,也越需要控制验证次数和速率。
具体窗口大小不是通用常数,应根据设备使用方式、通信可靠性和安全要求决定。接收端还可以采用单独的重新同步流程:用户在短时间内按指定步骤操作,接收端才接受更远的计数器。不要为了方便而无限扩大窗口,否则会削弱对异常前进的约束。
计数器回绕也必须处理。固定宽度计数器最终会达到最大值;不能直接让它归零并继续使用旧密钥,否则接收端可能把新消息误判为旧消息。系统应规定在耗尽前更换密钥、重新配对,或使用能安全处理周期和状态迁移的协议。
可运行示例:用 HMAC 和计数器演示核心机制
下面的 Python 示例使用标准库中的 HMAC-SHA-256。发送端生成一个包含计数器、命令和认证标签的消息;接收端在有限窗口内验证消息,并拒绝旧计数器。把代码保存为 rolling_code_demo.py,使用 Python 3 运行即可:
import hashlib
import hmac
import secrets
# 演示用的共享密钥。真实系统不得把密钥写死在源码中。
KEY = secrets.token_bytes(32)
TAG_BYTES = 16
WINDOW = 32
def make_tag(key: bytes, counter: int, command: str) -> bytes:
"""为计数器和命令生成截断的 HMAC-SHA-256 标签。"""
message = f"{counter}:{command}".encode("utf-8")
return hmac.new(key, message, hashlib.sha256).digest()[:TAG_BYTES]
def transmit(key: bytes, counter: int, command: str) -> dict:
"""模拟发送端生成一条消息。"""
return {
"counter": counter,
"command": command,
"tag": make_tag(key, counter, command),
}
class Receiver:
def __init__(self, key: bytes, last_counter: int = 0):
self.key = key
self.last_counter = last_counter
def accept(self, packet: dict) -> bool:
counter = packet["counter"]
command = packet["command"]
tag = packet["tag"]
# 先限制类型和计数器范围,避免把不可信输入直接用于后续处理。
if not isinstance(counter, int) or counter <= self.last_counter:
return False
if counter > self.last_counter + WINDOW:
return False
if not isinstance(command, str) or not isinstance(tag, bytes):
return False
expected = make_tag(self.key, counter, command)
if len(tag) != TAG_BYTES or not hmac.compare_digest(tag, expected):
return False
# 验证成功后才更新状态;实际系统必须可靠持久化此状态。
self.last_counter = counter
return True
if __name__ == "__main__":
receiver = Receiver(KEY)
first = transmit(KEY, 1, "OPEN")
print("首次接收:", receiver.accept(first)) # True
print("重放同一消息:", receiver.accept(first)) # False
skipped = transmit(KEY, 4, "OPEN")
print("窗口内跳过若干次:", receiver.accept(skipped)) # True
too_far = transmit(KEY, 100, "OPEN")
print("超出窗口:", receiver.accept(too_far)) # False
forged = transmit(KEY, 5, "OPEN")
forged["tag"] = b"\\x00" * TAG_BYTES
print("伪造标签:", receiver.accept(forged)) # False
这里的 KEY 在一次程序运行期间同时供发送端和接收端使用,仅为了让示例短小。现实设备中,发送端和接收端要通过安全配对获得密钥,不能依赖源码中的常量,也不应使用容易猜测的口令直接充当密钥。示例里的 secrets.token_bytes(32) 会随机生成密钥,因此每次运行都会得到不同结果,旧运行产生的消息不能拿到新运行中验证。
WINDOW 是示例允许接收端向前搜索的最大步数,并非协议的普遍推荐值。这个实现直接收到计数器后计算该位置的 HMAC,因此它演示的是“有限前向范围内接受更新计数器”。在真实系统中,还需对消息长度、字段编码、计数器位宽和命令值做明确约束,避免解析歧义或异常输入导致问题。
示例也展示了一个重要顺序:只有标签有效并且计数器符合规则时,接收端才推进状态。若先更新计数器、后验证标签,攻击者可能仅凭无效消息就让设备失步。示例使用 hmac.compare_digest 比较标签,避免使用普通字符串或字节相等比较来处理认证值。
适用场景
- 车库门和车辆遥控:主要风险之一是无线信号被录制后重放。滚动代码能让已经使用过的遥控消息失效。
- 无线门禁和控制器:在受限设备上,计数器和消息认证码可以作为较轻量的防重放机制,但必须结合设备身份和安全的密钥管理。
- 低功耗遥控设备:设备不一定能维持持续在线连接,计数器式状态可在离线情况下工作。代价是发送端和接收端都必须妥善保存状态。
- 其他需要一次性使用凭据的协议:滚动代码的思路可帮助理解“每条消息带有新鲜度信息”的设计,但不同场景对同步、保密和身份认证的要求并不相同,不能直接照搬遥控器方案。
如果系统能始终在线并采用请求—响应交互,也可以使用随机数(nonce)、服务器端挑战或其他新鲜度机制。滚动计数器不是所有防重放问题的唯一解法;选择方案时要看设备是否离线、是否有可信时钟、消息是否可能乱序,以及状态能否可靠保存。
滚动代码的安全边界
它不是加密,也不能自动隐藏命令
如果消息中的命令是明文,监听者可能知道设备收到的是“开门”还是“关门”。使用 HMAC 可以验证消息完整性和来源,却不会加密消息。若命令内容也属于敏感信息,需要设计加密和认证方案,并正确处理随机数、初始化向量或 nonce,不能简单地把字段“加密一下”就认为安全。
它不能弥补密钥泄露
滚动代码依赖密钥。密钥若被从固件、调试接口、备份文件或制造流程中提取,攻击者可能生成看似有效的新消息。设备应尽可能使用受保护的密钥存储,并限制调试和密钥导出能力。密钥管理包括生成、分发、更新、吊销和设备报废,不能只关注算法本身。
它不能阻止所有实时攻击
防重放并不等于防止实时转发。攻击者可能把一条尚未使用的合法消息实时转发给接收端,或者干扰正常通信。系统还需考虑无线干扰、拒绝服务和物理攻击。对于高风险的解锁或支付操作,单向遥控式滚动代码通常不能单独满足完整的安全要求。
它不能自动解决设备克隆和弱配对
如果设备配对时采用固定默认密钥、可预测的设备编号,或者通过未认证的无线流程接受新密钥,攻击者可能绕过滚动机制建立自己的发送端。首次配对、换机、重置和授权撤销都应有明确的安全流程。
常见错误与改进方式
只递增数字,不做消息认证
把计数器直接当作“滚动代码”,并不构成可靠的认证。计数器通常可以被监听,未认证的命令和状态也可能被修改。应使用经过审查的消息认证算法,把设备身份、计数器和命令等重要字段都纳入认证范围。
把计数器或短验证码当作密钥
计数器解决的是新旧顺序,验证码只是消息的一部分,都不应替代高熵密钥。不要使用设备序列号、固定口令或少量数字作为 HMAC 密钥。示例中的 32 字节随机密钥是演示做法;生产环境还应根据平台能力和密钥管理体系选择合适的密钥长度与存储方式。
窗口过大,或不限制错误尝试
大窗口能容忍更多丢包,却会增加接收端需要考虑的状态范围。若认证检查可以被大量尝试,还应设置速率限制、异常监控或延迟策略。窗口大小应经过实际通信测试和威胁分析确定,既不能为了省事设成无限,也不能小到让正常使用频繁失步。
验证失败也推进计数器
接收端应先完成格式检查、标签验证和新鲜度检查,再更新已接受状态。任何一步失败,都不应执行命令或推进计数器。状态更新最好具备原子性:设备突然断电时,不应出现“命令已经执行但计数器没有保存”或相反的状态不一致。
重启后丢失接收状态
如果接收端每次启动都把已接受计数器恢复到固定初值,之前捕获的旧消息可能再次被接受。计数器状态要可靠持久化,并考虑掉电写入、闪存磨损和多接收端共享等问题。可以使用适合硬件的事务式存储或状态版本设计,但具体实现要结合设备平台验证。
随意重置发送端计数器
更换电池、恢复出厂设置或更换接收器时,计数器可能被重置。若新旧状态重叠,接收端可能拒绝合法遥控器,也可能在设计不当时重新接受旧消息。应明确定义重新配对和重新同步流程,并保证重置操作经过授权。
把示例代码直接用于产品
示例只说明计数器、HMAC、有限窗口和拒绝重放的关系。它没有实现无线帧格式、设备身份绑定、密钥轮换、持久化、防故障更新、命令授权、抗侧信道措施或完整的配对协议。产品开发应使用经过同行评审的密码库与协议设计,并进行代码审查和安全测试,而不是把演示脚本改几行就部署。
设计与排查时的检查清单
- 每条消息是否认证了命令、计数器及必要的设备标识?
- 认证标签长度是否足够,消息编码是否唯一且无歧义?
- 接收端是否拒绝重复和过旧消息,并限制可接受的前向窗口?
- 验证失败时是否完全不更新状态、不执行命令?
- 发送端和接收端状态是否能在重启、断电后正确恢复?
- 配对、重置、换钥和设备吊销是否有经过授权的流程?
- 是否考虑计数器耗尽、丢包、乱序、多接收端和存储故障?
- 是否把防重放与加密、抗实时转发、抗干扰等不同目标区分开?
总结
滚动代码的核心是让每次认证消息都带有可验证的新鲜度,并由接收端记录已接受的位置,从而拒绝简单重放。实际系统通常需要把计数器与密钥和消息认证码结合,再通过有限同步窗口处理丢包。它适合许多离线遥控场景,但不是加密的替代品,也不能解决密钥泄露、弱配对、实时转发或拒绝服务等所有问题。
设计时最值得优先保证的,是可靠的密钥管理、严格的消息认证、合理的窗口、不可回退的持久状态,以及经过保护的配对和恢复流程。示例代码可以帮助理解这些原则;真正的产品协议仍应依据风险和平台条件设计、审查与测试。




