HOTP 算法与实现解析

HOTP 算法详解

说在前面

之前在2FA_Tool工具当中实现了TOTP算法的实现,后来我又使用到了HOTP算法的场景,现在已经在工具当中实现,现在对于HOTP算法进行详细解读。

1. 前言

HOTP(HMAC-based One-Time Password,基于 HMAC 的一次性密码)是一种广泛应用于身份验证的安全机制,是许多双因素认证系统的基础。与 TOTP(基于时间)不同,HOTP 是基于计数器的一次性密码算法。

本文将详细解析 HOTP 类的实现,包括 HOTP 验证码的生成和密钥验证过程。

2. 技术简介

HOTP 是 RFC 4226 定义的标准算法,其核心原理是基于一个共享密钥和递增的计数器值,通过 HMAC 算法生成一次性密码。HOTP 密码通常为 6 位数字,每次认证后计数器递增,确保每次生成的密码都不同。

3. HOTP 算法原理讲解

HOTP 算法通过以下公式定义:

scss 复制代码
HOTP(K, C) = Truncate(HMAC-SHA-1(K, C)) 

其中:

  • K 是共享密钥
  • C 是计数器值
  • Truncate 是动态截断函数,将 HMAC 结果转换为数字

HOTP 的安全性依赖于:

  1. 密钥的保密性
  2. HMAC 算法的单向性
  3. 计数器的严格递增

4. 代码结构介绍

HOTP 类包含两个静态方法:generate_hotpvalidate_secret,分别用于生成 HOTP 验证码和验证 Base32 编码的密钥。

4.1 generate_hotp 方法

generate_hotp 方法用于生成基于计数器的一次性密码。其核心流程如下:

python 复制代码
@staticmethod
def generate_hotp(secret: str, counter: Union[int, str]) -> str:
    # 解码 Base32 密钥
    try:
        key = base64.b32decode(secret.upper())
    except Exception:
        raise ValueError("无效的 Base32 密钥")

    # 验证并转换计数器为整数
    try:
        counter_int = int(counter)
    except (TypeError, ValueError):
        raise ValueError("计数器必须是整数或数字字符串")

    # 打包计数器(8 字节大端)
    counter_bytes = struct.pack(">Q", counter_int)

    # 生成 HMAC-SHA1
    hmac_hash = hmac.new(key, counter_bytes, hashlib.sha1).digest()

    # 动态截断
    offset = hmac_hash[-1] & 0x0F
    truncated_hash = hmac_hash[offset:offset + 4]
    code_int = struct.unpack(">I", truncated_hash)[0] & 0x7FFFFFFF

    # 返回 6 位验证码
    return f"{code_int % 1_000_000:06}"
4.1.1 解码 Base32 密钥

第一步是对传入的 secret(Base32 编码的密钥)进行解码。base64.b32decode 函数用于将密钥从 Base32 编码转换为二进制数据,这里的解码是和TOTP类当中的实现是一样的。

python 复制代码
try:
    key = base64.b32decode(secret.upper())
except Exception:
    raise ValueError("无效的 Base32 密钥")

如果密钥格式不正确,程序会抛出异常并提示用户 "无效的 Base32 密钥"。

4.1.2 处理计数器值

HOTP 使用计数器而非时间戳作为变量。计数器可以是整数或数字字符串:

python 复制代码
try:
    counter_int = int(counter)
except (TypeError, ValueError):
    raise ValueError("计数器必须是整数或数字字符串")
4.1.3 打包计数器值

计数器值需要转换为 8 字节的大端序字节串:

python 复制代码
counter_bytes = struct.pack(">Q", counter_int)

>Q 表示:

  • > 表示大端序
  • Q 表示 8 字节无符号长整型
4.1.4 生成 HMAC-SHA1 哈希

使用 HMAC-SHA1 算法生成哈希值:

python 复制代码
hmac_hash = hmac.new(key, counter_bytes, hashlib.sha1).digest()
4.1.5 动态截断处理

HOTP 使用动态截断(Dynamic Truncation)从 HMAC 结果中提取有效部分:

  1. 取最后一个字节的低 4 位作为偏移量:
python 复制代码
offset = hmac_hash[-1] & 0x0F
  1. 从偏移量开始取 4 字节:
python 复制代码
truncated_hash = hmac_hash[offset:offset + 4]
  1. 转换为整数并屏蔽最高位:
python 复制代码
code_int = struct.unpack(">I", truncated_hash)[0] & 0x7FFFFFFF
4.1.6 生成 6 位验证码

最后将整数模 1,000,000 并格式化为 6 位数字:

python 复制代码
return f"{code_int % 1_000_000:06}"

4.2 validate_secret 方法

validate_secret 方法与 TOTP 中的实现相同,用于验证 Base32 密钥的有效性:

python 复制代码
@staticmethod
def validate_secret(secret: str) -> bool:
    """
    验证 Base32 密钥是否合法
    :param secret: Base32 编码的密钥
    :return: True 或抛出 ValueError
    """
    try:
        base64.b32decode(secret.upper())
        return True
    except Exception:
        raise ValueError("无效的 Base32 密钥")

5. 完整 HOTP 实现代码

python 复制代码
class HOTP:
    @staticmethod
    def generate_hotp(secret: str, counter: Union[int, str]) -> str:
        """
        生成 HOTP 验证码
        :param secret: Base32 编码的密钥
        :param counter: 计数器值(整数或数字字符串)
        :return: 6 位验证码,左侧补零
        :raises ValueError: 密钥或计数器无效时抛出
        """
        # 解码 Base32 密钥
        try:
            key = base64.b32decode(secret.upper())
        except Exception:
            raise ValueError("无效的 Base32 密钥")

        # 验证并转换计数器为整数
        try:
            counter_int = int(counter)
        except (TypeError, ValueError):
            raise ValueError("计数器必须是整数或数字字符串")

        # 打包计数器(8 字节大端)
        counter_bytes = struct.pack(">Q", counter_int)

        # 生成 HMAC-SHA1
        hmac_hash = hmac.new(key, counter_bytes, hashlib.sha1).digest()

        # 动态截断
        offset = hmac_hash[-1] & 0x0F
        truncated_hash = hmac_hash[offset:offset + 4]
        code_int = struct.unpack(">I", truncated_hash)[0] & 0x7FFFFFFF

        # 返回 6 位验证码
        return f"{code_int % 1_000_000:06}"

    @staticmethod
    def validate_secret(secret: str) -> bool:
        """
        验证 Base32 密钥是否合法
        :param secret: Base32 编码的密钥
        :return: True 或抛出 ValueError
        """
        try:
            base64.b32decode(secret.upper())
            return True
        except Exception:
            raise ValueError("无效的 Base32 密钥")
相关推荐
程序员爱钓鱼几秒前
Go语言同步原语与数据竞争:WaitGroup
后端·google·go
重庆小透明4 小时前
【从零开始学习JVM | 第六篇】运行时数据区
java·jvm·后端·学习
大米2H5 小时前
Jupyter lab 配置两个python环境
ide·python·jupyter
你的人类朋友6 小时前
🤔Token 存储方案有哪些
前端·javascript·后端
烛阴6 小时前
从零开始:使用Node.js和Cheerio进行轻量级网页数据提取
前端·javascript·后端
猎嘤一号6 小时前
使用 PyTorch 和 TensorBoard 实时可视化模型训练
人工智能·pytorch·python
liuyang___6 小时前
日期的数据格式转换
前端·后端·学习·node.js·node
Takina~7 小时前
python打卡day49
python
Frankabcdefgh7 小时前
Python基础数据类型与运算符全面解析
开发语言·数据结构·python·面试
是梦终空7 小时前
Python毕业设计226—基于python+爬虫+html的豆瓣影视数据可视化系统(源代码+数据库+万字论文)
爬虫·python·html·毕业设计·毕业论文·源代码·豆瓣影视数据可视化