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 密钥")
相关推荐
赱向远方1 分钟前
【软件推荐——pdf2docx】
python·pdf·开源软件·docx·软件推荐·doc·pdf2docx
编程乐学(Arfan开发工程师)24 分钟前
10、底层注解-@Conditional条件装配
java·spring boot·后端·架构
半青年1 小时前
Qt读取Excel文件的技术实现与最佳实践
c语言·c++·python·qt·c#·excel
无闻墨客1 小时前
数据分析与应用---数据可视化基础
python·信息可视化·数据挖掘·数据分析·matplotlib
xiaohanbao091 小时前
day30 python 模块、包与库的高效使用指南
人工智能·python·学习·算法
Q_Q19632884751 小时前
python动漫论坛管理系统
开发语言·spring boot·python·django·flask·node.js·php
liuweidong08021 小时前
【Pandas】pandas DataFrame mode
python·数据挖掘·pandas
帮帮志1 小时前
vue3与springboot交互-前后分离【验证element-ui输入的内容】
spring boot·后端·ui
Sonetto19992 小时前
【Python】【面试凉经】Fastapi为什么Fast
python·面试·flask·fastapi·凉经
计算机学姐2 小时前
基于SpringBoot的小型民营加油站管理系统
java·vue.js·spring boot·后端·mysql·spring·tomcat