摘要
本文根据 RFC4226 和 RFC6238 文档,详细的介绍 HOTP 和 TOTP 算法的原理和实现。
两步验证已经被广泛应用于各种互联网应用当中,用来提供安全性。对于如何使用两步验证,大家并不陌生,无非是开启两步验证,然后出现一个二维码,使用支持两步验证的移动应用比如
Google Authenticator 或者 LassPass Authenticator
扫一下二维码。这时候应用会出现一个6位数的一次性密码,首次需要输入验证从而完成开启过程。以后在登陆的时候,除了输入用户名和密码外,还需要把当前的移动应用上显示的6位数编码输入才能完成登陆。
这个过程的背后主要由两个算法来支撑:HOTP 和 TOTP。也分别对应着两份 RFC 协议 RFC4266 和 RFC6238。前者是 HOTP 的标准,后者是
TOTP 的标准。本文将使用图文并茂的方式详细介绍 HOTP 和 TOTP 的算法原理,并在最后分析其安全性。当然所有内容都是基于协议的,通过自己的理解更加直观的表达出来。
协议解决的核心问题
通过前面两步验证的使用场景分析,不难看出问题的核心在于如何能够让用户手机应用产生的验证码和服务器产生的验证码一致,或者是在一定范围内一致。参考下图:
所以我们的算法就是在解决如何更好的生成这个验证码,既能保证服务器端和客户端同步,还能保证验证码不重复并且不容易被别人反向破解出共享密钥。其中如果是计数,则是
HOTP, 如果是使用时间来生成验证码,则是 TOTP。
HOTP 算法图解
符号定义
对于 HOTP,通过上图我们已经看到输入算法的主要有两个元素,一个是共享密钥,另外一个是计数。在 RFC 算法中用一下字母表示:
- K 共享密钥,这个密钥的要求是每个 HOTP 的生成器都必须是唯一的。一般我们都是通过一些随机生成种子的库来实现。
- C 计数器,RFC 中把它称为移动元素(moving factor)是一个 8个 byte的数值,而且需要服务器和客户端同步。
另外一个参数比较好理解,
- Digit 表示产生的验证码的位数
最后两个参数可能暂时不好理解,我们先放在这,等用到在解释
- T 称为限制参数(Throttling Parameter)表示当用户尝试 T 次 OTP 授权后不成功将拒绝该用户的连接。
- s 称为重新同步参数(Resynchronization Parameter)表示服务器将通过累加计数器,来尝试多次验证输入的一次性密码,而这个尝试的次数及为
s。该参数主要是有效的容忍用户在客户端无意中生成了额外不用于验证的验证码,导致客户端和服务端不一致,但同时也限制了用户无限制的生成不用于验证的一次性密码。
基础知识
javax.crypto.Mac
javax.crypto.Mac 是 Java 中用于计算消息认证码(MAC)的类。MAC 是一种用于验证消息完整性和真实性的技术,通常在数据传输和通信中使用。
具体来说,javax.crypto.Mac 类提供了以下功能:
- MAC 计算:它允许你使用指定的密钥来计算消息的消息认证码。MAC 是通过将消息和密钥作为输入来生成的,确保了消息的完整性和未被篡改。
- 多种算法支持:Java的 javax.crypto.Mac 类支持多种不同的MAC算法,例如HMAC(基于哈希函数的MAC)等。
- 灵活性:它允许你根据需要使用不同的密钥来计算不同的消息的MAC。这对于确保数据的安全性和验证消息来源非常有用。
- 提供完整性:MAC 算法可以防止数据在传输过程中被篡改。如果接收到的消息的MAC与预期的MAC不匹配,那么可以确定消息已被篡改。
总之,javax.crypto.Mac 类是Java中用于实现消息认证码的工具,有助于确保数据的完整性和验证消息的真实性。这对于安全通信和数据传输非常重要。
javax.crypto.Mac验证消息完整性
当验证消息的完整性时,通常使用 HMAC(基于哈希函数的消息认证码)。下面是一个使用 javax.crypto.Mac 来计算和验证消息完整性的简单Java示例:- import javax.crypto.Mac;
- import javax.crypto.spec.SecretKeySpec;
- import java.security.Key;
- import java.security.NoSuchAlgorithmException;
- import java.util.Arrays;
- public class MessageIntegrityVerification {
- public static void main(String[] args) throws Exception {
- // 1. 选择一个MAC算法(这里使用HMAC-SHA256)
- String algorithm = "HmacSHA256";
- // 2. 准备密钥
- String secretKey = "YourSecretKey"; // 替换成你的密钥
- Key key = new SecretKeySpec(secretKey.getBytes(), algorithm);
- // 3. 创建MAC对象并初始化
- Mac mac = Mac.getInstance(algorithm);
- mac.init(key);
- // 4. 要验证的消息
- String message = "This is a message to be verified.";
- // 5. 计算MAC
- byte[] macBytes = mac.doFinal(message.getBytes());
- // 6. 发送消息和MAC给接收方
- // 接收方将使用相同的密钥和消息来计算MAC,并比较它与接收到的MAC是否匹配
- // 模拟接收方验证
- boolean isValid = verifyMAC(key, message, macBytes);
- if (isValid) {
- System.out.println("消息完整性验证通过。");
- } else {
- System.out.println("消息完整性验证失败。");
- }
- }
- // 验证MAC的方法
- public static boolean verifyMAC(Key key, String message, byte[] receivedMAC) throws Exception {
- Mac mac = Mac.getInstance(key.getAlgorithm());
- mac.init(key);
- byte[] calculatedMAC = mac.doFinal(message.getBytes());
- // 使用MessageDigest的isEqual方法来比较两个MAC是否相等
- return Arrays.equals(calculatedMAC, receivedMAC);
- }
- }
复制代码 在上述示例中,我们选择了HMAC-SHA256作为MAC算法,准备了一个密钥,然后计算消息的MAC。在实际应用中,你将消息和MAC发送给接收方,接收方可以使用相同的密钥和消息来验证消息的完整性。
totp实例中的应用
[code] /** * 生成key. * @param password * @return * @throws UnsupportedEncodingException */ protected Key generateKey(String password) throws UnsupportedEncodingException { byte[] keyBytes = password.getBytes("UTF-8"); SecretKeySpec signingKey = new SecretKeySpec(keyBytes, this.mac.getAlgorithm()); return signingKey; } /** * Generates a one-time password using the given key and counter value. */ public synchronized int generateOneTimePassword(String password, final long counter) throws Exception { Key key = generateKey(password); this.mac.init(key); this.buffer[0] = (byte) ((counter & 0xff00000000000000L) >>> 56); this.buffer[1] = (byte) ((counter & 0x00ff000000000000L) >>> 48); this.buffer[2] = (byte) ((counter & 0x0000ff0000000000L) >>> 40); this.buffer[3] = (byte) ((counter & 0x000000ff00000000L) >>> 32); this.buffer[4] = (byte) ((counter & 0x00000000ff000000L) >>> 24); this.buffer[5] = (byte) ((counter & 0x0000000000ff0000L) >>> 16); this.buffer[6] = (byte) ((counter & 0x000000000000ff00L) >>> 8); this.buffer[7] = (byte) (counter & 0x00000000000000ffL); this.mac.update(this.buffer, 0, 8); try { this.mac.doFinal(this.buffer, 0); } catch (final ShortBufferException e) { // We allocated the buffer to (at least) match the size of the MAC length at // construction time, so this // should never happen. throw new RuntimeException(e); } final int offset = this.buffer[this.buffer.length - 1] & 0x0f; return ((this.buffer[offset] & 0x7f) 56);</p>这行代码的实际意义是从一个长整数 counter 中提取出特定的字节,然后将其转换为字节类型 (byte)。
让我解释这行代码的各个部分:
- counter & 0xff00000000000000L:这部分是一个位运算,使用与操作 & 和一个掩码 0xff00000000000000L
,它的作用是保留 counter 的最高字节(8个比特)并将其他字节清零。
- >>> 56:这部分是无符号右移操作,它将上面结果的字节向右移动,使得最终的结果在字节的最低位置。
所以,(byte) ((counter & 0xff00000000000000L) >>> 56) 这行代码提取了 counter
的最高字节,并将其作为一个字节值返回。这通常用于将长整数的不同字节部分转换为字节数据,以便后续用于计算消息验证码等操作。
this.mac.update(this.buffer, 0, 8) 使用 this.buffer 中的数据来更新 MAC 实例,以便进行 MAC 计算。
this.mac.doFinal(this.buffer, 0) 完成 MAC 计算,将结果存储在 this.buffer 中。
最后,通过 try 和 catch 块捕获 ShortBufferException 异常,如果发生异常,会抛出一个 RuntimeException
。这是因为在构造时,this.buffer 被分配了足够大的空间来存储 MAC 的结果,所以不应该发生短缓冲区异常。
</ol>总之,这段代码的主要作用是使用给定的密钥和计数器生成一个消息验证码,以确保消息的完整性和真实性。
算法流程
核心步骤主要是使用 K C 和 Digit。
- 第一步:使用 HMAC-SHA-1 算法基于 K 和 C 生成一个20个字节的十六进制字符串(HS)。关于如何生成这个是另外一个协议来规定的,RFC
2104 HMAC Keyed-Hashing for Message Authentication. 实际上这里的算法并不唯一,还可以使用 HMAC-SHA-256 和 HMAC-SHA-512
生成更长的序列。对应到协议中的算法标识就是
HS = HMAC-SHA-1(K,C)
- 第二步:选择这个20个字节的十六进制字符串(HS 下文使用 HS 代替 )的最后一个字节,取其低4位数并转化为十进制。比如图中的例子,最后的字节是
5a,第四位就是 a,十六进制也就是 0xa,转化为十进制就是 10。该数字我们定义为 Offset,也就是偏移量。
- 第三步:根据偏移量 Offset,我们从 HS 中的第 10(偏移量)个字节开始选取 4 个字节,作为我们生成 OTP 的基础数据。图中例子就是选择
50ef7f19,十六进制表示就是 0x50ef7f19,我们成为 Sbits
以上两步在协议中的成为 Dynamic Truncation (DT)算法,具体参考以下伪代码:
[code]Let Sbits = DT(HS) // DT, defined below,// returns a 31-bit string展开就是DT(String) // String = String[0]...String[19]Let OffsetBits be the low-order 4 bits of String[19]Offset = StToNum(OffsetBits) // 0 |