天空 Blond - OTP
type: channel
link: https://skyblond.info/tag/OTP/
items: 【歪门邪道】一种基于时间的OTP验证方法

【歪门邪道】一种基于时间的OTP验证方法
type: item
date: 2019-07-22T21:14:23+08:00
description: 本来是想着就最近的事情写点什么，但是总是没有合适的时间。从暑假开始一直到现在都在忙着给团委写代码，早上10点起床晚上11点上床，其间除了吃饭就是写代码，11点到凌晨2点看舍长的杀手2直播录像，看困了睡觉。最近发生的事情可能会在几个星期后再写吧，尽管那时侯可能好多人已经忘记了。总之书归正传，这次写的一个方法是应用在我手头的项目上的，我感觉我应该不是原创，毕竟有很多类似的方法，大体思路相同的那种。0x01 思路这个方法的创立是受到Yubico OTP的启发。不为众所周知：我最近公费买了个Yubikey 5 NFC，发现还挺有意思的，它的使用方法，例如FIDO、OTP、静态密码和OpenPGP之类的部分在网上都已经有很详尽的说明了，如果只是记述我重复人家的操作大概会很没意思，所以关于这个的文章我就没写。但是简单地说，Yubico OTP是Yubico自己家的动态验证码。这里可以找到官方的解释。简而言之就是Yubikey设备内会持有一个AES密钥和一个计数器，而Yubico官方的验证服务器也同样持有AES密钥和一个计数器。每次Yubikey产生的OTP字符串都是经过AES加密的，其中含有计数器信息。如果一个字符串能够被正确解密且计数器次数是大于服务器上储存的次数的话，就认为验证通过，否则就是重放攻击。但其实这样的机制也不是安全，例如试先储存好生成的字符串，在你本人利用新的字符串验证身份之前（此时服务器上的计数还未更新），我（假设是攻击者）就已经利用这些预先准备的字符串向服务器提交，同样会验证通过，而你之后也不会发现（假设验证后第三方不会记录登陆日志等信息）。但着手于手头项目的需要：一个动态变化的URL能够指向同一个实体，并且要求这个URL是时间新鲜1的。因此我想到了使用类似Yubico OTP的方法来生成这个URL。0x02 细节首先URL中可变的部分称为token，一个合法的token是44个字符长。其中前12个字符标识一个具体唯一的实体，称为uid；后32个字符是仅针对该实体和当前时间有效的，称为payload。uid可以自己定，只要保证一个uid对应唯一一个可辨识的实体即可，其长度也可以随意，我选择12个字符长度。而payload则是利用实体持有的AES密钥和生成的初始化向量进行加密，加密内容是当前的时间戳。其中初始化向量一共16字节，前8个字节是使用URL安全的Base64编码过的AES密钥的前八个字符，后8个字节是当前时间戳（long型）对应的字节。这个初始化向量可以凭自己喜好定义，但是如果要求对时间敏感，则最好不要把初始化向量设定为固定的。当服务器验证时首先分离uid和payload，通过uid查询到实体对应的AES密钥，使用其和相同算法生成的初始化向量解密，如果能够正确解密得到时间戳，则认为验证通过。这里为了容纳服务器和客户端之间的时间差，服务器应当验证当前时间戳、上一个时间戳和下一个时间戳下不同的可能性，当其中任一情况下通过验证，则可以认为token有效。0x03 编程实现（Java）工具类package info.skyblond.velvet.scarlatina; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.crypto.*; import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; import java.util.Base64; import java.util.Objects; /** * Handle things related with tokens */ public class Token { private static final Logger logger = LoggerFactory.getLogger(Token.class); private SecretKeySpec keySpec = null; /** * @param base64UrlSafeKey The base64(URL safe) encoded key which will be used in following operations. */ public To…