一日一技:如何实现临时密码?
我买的房子今天交房了。开发商配的门锁是某品牌的智能门锁,它可以使用指纹开锁,也可以使用密码开锁。在使用手机跟门锁配对以后,可以远程在手机上生成临时密码。临时密码只能使用1次,并且在生成的30分钟内有效。这个功能可以方便装修人员进出又不用担心泄露密码。
因为新房子还没有通网,所以门锁肯定是无法连接互联网的。而装修人员给我打电话要临时密码时,我在公司,离家几十公里外,门锁也不可能跟手机通信。
那么问题来了,门锁是怎么验证这个临时密码合法的?
今天我一直在想这个问题,目前有一些思路,但无法确定。所以发出来跟大家一起讨论一下它的实现方法。
已知:
- 手机App只有第一次跟门锁配对时,会通信,之后就完全不会有任何通信
- 门锁无法连接外网
- 无论我在任何地方,手机上都能生成临时密码。门锁输入临时密码就能解锁
- 临时密码只能使用一次,之后就会失效
- 临时密码是8位数字
- 临时密码有效期30分钟,超时以后就会失效
- 手机可以连续多次生成临时密码,每一次密码都不一样,但每个临时密码都可以使用
首先第4条非常简单,在门锁里面记录一下已经使用的密码就可以实现密码只能使用1次。所以不需要考虑这个问题了。
另外几个问题,我根据我自己的编程经验做一些推测。
临时密码是一个8位数字,例如8031 1257
。由于手机不需要跟门锁通信,门锁就能够识别这个密码,因此我一开始觉得这个8位数字包含某种校验规则。例如,前4个数字,乘以100以后对26取余数,就是第5、6位数字。前6个数乘以5643然后对97取余数,就是第7、8位数字。这里的四个关键数字100
、26
、5643
、97
,可能是手机在和门锁配对的时候发送给门锁的。
但这里无法解释门锁怎么知道数字什么时候过期。难道8位数字能够包含精确到分钟的时间戳信息?例如现在我写文章的时候,对应的时间戳是1740143928
。这是一个10位数字,我实在想不到如何把一个10位的数字藏在8位数字里面,并且在必要的时候还能还原回原来的10位数字。
因此,我换了一个思路,有没有可能密码锁里面自带一个时钟?在手机配对时,会同步校准这个时钟,使它跟手机保持相同的时间。如果是这种方案,那么这个临时密码8位数字,其实可以不用包含自我校验。我们可以使用app和密码锁各自按相同的逻辑走一轮加密,然后对比生成的密钥是否相同。
手机在跟密码锁配对时,发送一个密钥到密码锁里面。要生成临时密码时,手机使用时间戳和密钥通过某个算法生成8个数字。密码锁也使用时间戳和这个相同的密钥,相同的算法,也生成8位数字,如果跟临时密码相同,就开锁。对应的Python密码类似如下:
1 | import hashlib |
运行效果如下图所示:
由于从任何时间戳x开始,x // 1800 == (x + n) // 1800
或x // 1800 + 1 == (x + n) // 1800
,其中0 <= n <= 1800
。密码锁使用相同的代码,只不过分别把timespan
和timespan + 1
都生成一个密码,然后对比,这样就可以实现手机生成密码过几分钟再在锁上面输入密码,锁也能成功验证。
使用这种方式,可以满足编号1-6
的需求。但问题是遇到需求7
怎么办?上面这种方式,会导致在30分钟内,临时密码只有这一个。
我绞尽脑汁想不出一个聪明的算法能解决这个问题。但是我在多次尝试生成临时密码时,发现两次密码的生成间隔必须大于5秒。
如果它的算法真的那么聪明,为什么不能让我生成无限个可用的临时密码?所以我怀疑它可能没有那么聪明,它可能用的是笨办法,例如:穷举。
对手机来说,生成密码的算法跟上面差不多,唯一的区别是,把其中的1800
换成5
。也就是说,每5秒钟会生成不同的临时密码。因为now // 5
在5秒钟内是相同的,超过5秒就会变。
而对于门锁,当它感知到用户正在输入密码时,以当前时间戳为起点,每5秒一轮,往前推360轮,生成360个密码。如果发现用户输入的8位数跟这360个密码中的某一个相同,就解锁。
如果锁的芯片性能好,在用户按完临时密码前,就能够做完360次计算,用户完全不会有任何感知。如果锁的芯片不够好,它可以在内存中维持一个长度为360的双向链表,每5秒计算一个密码放进去,然后把末尾的密码丢掉。这样当用户输入密码的时候,它可以直接跟这个双向链表中的密码逐一比对。
当然,以上全都是我个人的推测。如果大家知道它是怎么做的,或者想到了什么更好的方法,欢迎留言一起交流。