【附源码】告别静态密码!openHiTLS 开源一次性密码协议(HOTP/TOTP),推动动态认证普及
1 前言
近日,openHiTLS社区正式发布基于RFC 4226(HOTP)与RFC 6238(TOTP)标准的双因素认证(2FA)新特性。这是国内密码开源领域在身份安全领域的关键技术突破,通过“动态一次性密码”机制构建“静态密码+动态验证”的纵深防护体系,可广泛应用于金融账户安全、企业权限管理、IoT设备认证等核心场景,为数字身份安全提供标准化、高可靠的技术支撑。
2 静态密码安全瓶颈凸显,动态认证成破局关键
随着数字化进程加速,传统静态密码的安全短板日益突出:一方面,“撞库攻击”、“钓鱼窃取”等手段导致密码泄露事件频发,且泄露后存在持久滥用风险;另一方面,单一认证方式难以抵御APT攻击、短信劫持等新型威胁,无法满足金融、企业等领域的合规与安全需求。
在此背景下,TOTP(Time-based One-Time Password)与HOTP(HMAC-based One-time Password)作为国际主流双因素认证技术,凭借“动态生成、一次有效”的核心特性,成为解决身份安全难题的核心方案。openHiTLS此次推出的新特性,正是针对这一技术需求,为广大行业用户提供符合国际标准的开源实现。
3 技术深度解析:双机制构建动态安全屏障
TOTP与HOTP均基于HMAC哈希算法构建,核心逻辑是通过“密钥+动态因子”生成唯一一次性密码,使认证过程从“静态固定”升级为“动态可变”,二者技术路径各有侧重。
3.1 HOTP:计数器驱动的事件同步认证
HOTP基于一个递增的计数器值C和一个只有鉴别双方持有的静态对称密钥K,使用HMAC-SHA-1算法。其生成算法为HOTP(K,C)=Truncate(HMAC(K,C)),一个6位HOTP简易生成流程如下图所示:
![]() |
图上算法步骤可分解如下
Step 1. 计算HMAC值
HS = HMAC(K,C):入参K是只有鉴别双方持有的静态对称密钥,入参C是鉴别双方同步的计数器值,为8字节整数,出参HS是一个20字节序列。
Step 2. 动态截断
Snum = Truncate(HS):入参HS是第Step1步的输出,Truncate()是截断函数,出参是一个32位整数(有效位为低31位)
Step 3. 计算HOTP值
HOTP= Snum mod 10^Digit:Snum 是Step2 的输出,Digit是HOTP位数,将Snum对10^Digit取模,并将结果格式化为指定位数的数字字符串,不足位数时在左侧补零。
3.2 TOTP:时间驱动的实时动态认证
TOTP基于当前时间因子T和一个只有鉴别双方持有的静态对称密钥K,依赖HMAC-SHA-1、HMAC-SHA-256或HMAC-SHA-512算法。其生成算法为TOTP=HOTP(K,T),其中T=(Current Unix time - T0) / X。详细算法流程如下:
Step 1. 计算当前时间因子
T=(Current Unix time - T0) / X:Current Unix time是当前的Unix时间戳,是一个8字节整数,入参T0是开始计算时间步长的初始Unix时间戳,为8字节整数,默认为0,入参X是以秒为单位的时间步长大小,默认为30,计算结果为除法向下取整结果,长度为8字节。
Step 2. 计算TOTP值
TOTP = HOTP(K, T):入参K是只有鉴别双方持有的静态对称密钥,而入参T是Step 1的输出。
4 多场景落地:从资金安全到设备防护的全维度覆盖
4.1 金融领域:合规级资金安全防护
针对网银、证券APP、第三方支付等场景,TOTP技术可替代传统短信验证码,构建“静态密码+动态码”的双重认证体系。即使静态密码泄露,攻击者因无法获取30秒内有效的TOTP动态码,无法完成登录或转账操作,有效防范盗刷风险,同时满足金融监管对多因素认证的合规要求。
4.2 企业场景:精细化权限安全管控
在企业OA、CRM系统及服务器运维场景中,可通过HOTP/TOTP实现分级认证:普通员工登录采用手机APP(TOTP)或硬件令牌(HOTP)验证;运维人员操作服务器需叠加SSH密钥与TOTP动态码的双重校验。员工离职后,管理员可快速注销共享密钥,实现权限即时回收,避免数据泄露风险。
4.3 IoT领域:终端身份伪造防御
针对智能门锁、工业传感器等IoT设备,TOTP技术可解决固定密钥通信的安全隐患。设备与云端共享密钥,每30秒生成一次TOTP并上传验证,云端仅允许携带有效动态码的设备接入网络或执行指令,有效抵御设备劫持与身份伪造攻击。
5 三重技术优势:安全、兼容与易用的平衡
- 安全性:基于HMAC-SHA1/SHA256算法(可扩展至国密SM3),结合“动态密码一次有效”特性,抗暴力破解、字典攻击能力突出;支持密钥加盐、动态窗口控制等增强机制,提升高级威胁对抗能力。
- 广兼容性:完全遵循国际标准,可无缝对接Google Authenticator、YubiKey等主流终端与Windows、Linux、iOS、Android等系统,无需重构现有认证体系;提供简洁API接口与开发文档,降低企业部署成本。
- 高易用性:用户通过手机APP或硬件令牌即可获取动态码,操作门槛低;支持多设备同步、二维码密钥备份等功能,兼顾安全性与可用性。
6 未来展望:共建数字身份安全新生态
TOTP/HOTP技术作为数字身份认证的“动态安全闸门”,其开源化落地将加速国内动态认证技术的普及与应用。openHiTLS社区诚邀广大开发者、企业用户加入,共同推进技术深度优化与场景创新,实现从“技术研发”到“产业落地”的高效转化,最终构建兼顾身份安全、合规监管与用户体验的全域数字身份防护体系。
如需了解更多技术细节或参与社区协作,可关注openHiTLS官网获取开发文档与最新动态。
【附源代码】
完整代码:https://gitcode.com/openHiTLS/openhitls/tree/main/auth/otp/src
#include <stdint.h>
#include "securec.h"
#include "bsl_errno.h"
#include "bsl_err_internal.h"
#include "bsl_sal.h"
#include "bsl_bytes.h"
#include "auth_errno.h"
#include "auth_params.h"
#include "auth_otp.h"
#include "crypt_errno.h"
#include "otp.h"int32_t HITLS_AUTH_OtpInit(HITLS_AUTH_OtpCtx *ctx, uint8_t *key, uint32_t keyLen)
{if (ctx == NULL || keyLen == 0) {BSL_ERR_PUSH_ERROR(HITLS_AUTH_OTP_INVALID_INPUT);return HITLS_AUTH_OTP_INVALID_INPUT;}if (ctx->key.data != NULL) {BSL_SAL_Free(ctx->key.data);}ctx->key.dataLen = keyLen;ctx->key.data = (uint8_t *)BSL_SAL_Malloc(ctx->key.dataLen);if (ctx->key.data == NULL) {BSL_ERR_PUSH_ERROR(BSL_MALLOC_FAIL);return BSL_MALLOC_FAIL;}if (key == NULL) {int32_t ret = ctx->method.random(ctx->key.data, ctx->key.dataLen);if (ret != CRYPT_SUCCESS) {BSL_SAL_Free(ctx->key.data);ctx->key.data = NULL;BSL_ERR_PUSH_ERROR(ret);return ret;}} else {(void)memcpy_s(ctx->key.data, ctx->key.dataLen, key, keyLen);}return HITLS_AUTH_SUCCESS;
}typedef enum {OTP_HMAC_SHA1SIZE = 20,OTP_HMAC_SHA256SIZE = 32,OTP_HMAC_SHA512SIZE = 64,
} OTP_HmacSize;int32_t GenericOtpGen(HITLS_AUTH_OtpCtx *ctx, uint64_t movingFactor, char *otp, uint32_t *otpLen)
{// Put movingFactor value into byte arrayuint8_t counter[sizeof(movingFactor)];for (uint32_t i = 0; i < sizeof(movingFactor); i++) {counter[sizeof(movingFactor) - 1 - i] = (movingFactor >> (8 * i)) & 0xFF; // 8: the number of bits in a byte.}// Compute HMAC hashuint8_t hmac[OTP_HMAC_SHA512SIZE];uint32_t hmacLen;switch (ctx->hashAlgId) {case HITLS_AUTH_OTP_CRYPTO_SHA1:hmacLen = OTP_HMAC_SHA1SIZE;break;case HITLS_AUTH_OTP_CRYPTO_SHA256:hmacLen = OTP_HMAC_SHA256SIZE;break;case HITLS_AUTH_OTP_CRYPTO_SHA512:hmacLen = OTP_HMAC_SHA512SIZE;break;default:BSL_ERR_PUSH_ERROR(HITLS_AUTH_OTP_INVALID_INPUT);return HITLS_AUTH_OTP_INVALID_INPUT;}uint32_t ret = ctx->method.hmac(ctx->libCtx, ctx->attrName, ctx->hashAlgId, ctx->key.data, ctx->key.dataLen,(uint8_t *)&counter, sizeof(counter), hmac, &hmacLen);if (ret != HITLS_AUTH_SUCCESS) {BSL_ERR_PUSH_ERROR(ret);return ret;}// Dynamic truncationuint8_t offset = hmac[hmacLen - 1] & 0x0F;uint32_t binOtp = BSL_ByteToUint32(&hmac[offset]) & 0x7FFFFFFF;// Stringifyfor (uint32_t i = 0, div = 10, mod = 1; i < ctx->digits; i++, div *= 10, mod *= 10) { // 10: decimal numberotp[ctx->digits - i - 1] = '0' + binOtp % div / mod;}*otpLen = ctx->digits;return HITLS_AUTH_SUCCESS;
}int32_t HotpGen(HITLS_AUTH_OtpCtx *ctx, const BSL_Param *param, char *otp, uint32_t *otpLen, uint64_t *movingFactorOut)
{uint64_t movingFactor = 0;uint32_t movingFactorLen = (uint32_t)sizeof(movingFactor);const BSL_Param *tmpParam = BSL_PARAM_FindConstParam(param, AUTH_PARAM_OTP_HOTP_COUNTER);if (tmpParam == NULL) {BSL_ERR_PUSH_ERROR(HITLS_AUTH_OTP_NO_COUNTER);return HITLS_AUTH_OTP_NO_COUNTER;}int32_t ret = BSL_PARAM_GetValue(tmpParam, AUTH_PARAM_OTP_HOTP_COUNTER, BSL_PARAM_TYPE_OCTETS, &movingFactor,&movingFactorLen);if (ret != BSL_SUCCESS) {BSL_ERR_PUSH_ERROR(ret);return ret;}if (movingFactorOut != NULL) {*movingFactorOut = movingFactor;}return GenericOtpGen(ctx, movingFactor, otp, otpLen);
}int32_t TotpGen(HITLS_AUTH_OtpCtx *ctx, const BSL_Param *param, const int32_t offset, char *otp, uint32_t *otpLen,uint64_t *movingFactorOut)
{uint64_t curTime = 0;uint32_t curTimeLen = (uint32_t)sizeof(curTime);const BSL_Param *tmpParam = BSL_PARAM_FindConstParam(param, AUTH_PARAM_OTP_TOTP_CURTIME);if (tmpParam == NULL) {BSL_ERR_PUSH_ERROR(HITLS_AUTH_OTP_NO_CURTIME);return HITLS_AUTH_OTP_NO_CURTIME;}int32_t ret =BSL_PARAM_GetValue(tmpParam, AUTH_PARAM_OTP_TOTP_CURTIME, BSL_PARAM_TYPE_OCTETS, &curTime, &curTimeLen);if (ret != BSL_SUCCESS) {BSL_ERR_PUSH_ERROR(ret);return ret;}TotpCtx *totpCtx = (TotpCtx *)ctx->ctx;uint64_t movingFactor = ((curTime - totpCtx->startOffset) / totpCtx->timeStepSize) + offset;if (movingFactorOut != NULL) {*movingFactorOut = movingFactor;}return GenericOtpGen(ctx, movingFactor, otp, otpLen);
}int32_t HITLS_AUTH_OtpGen(HITLS_AUTH_OtpCtx *ctx, const BSL_Param *param, char *otp, uint32_t *otpLen)
{if (ctx == NULL || ctx->key.data == NULL || param == NULL || otp == NULL || otpLen == NULL) {BSL_ERR_PUSH_ERROR(HITLS_AUTH_OTP_INVALID_INPUT);return HITLS_AUTH_OTP_INVALID_INPUT;}if (*otpLen < ctx->digits) {BSL_ERR_PUSH_ERROR(HITLS_AUTH_OTP_INVALID_INPUT);return HITLS_AUTH_OTP_INVALID_INPUT;}switch (ctx->protocolType) {case HITLS_AUTH_OTP_HOTP:return HotpGen(ctx, param, otp, otpLen, NULL);case HITLS_AUTH_OTP_TOTP:return TotpGen(ctx, param, 0, otp, otpLen, NULL);default:BSL_ERR_PUSH_ERROR(HITLS_AUTH_OTP_INVALID_PROTOCOL_TYPE);return HITLS_AUTH_OTP_INVALID_PROTOCOL_TYPE;}
}int32_t HotpValidate(HITLS_AUTH_OtpCtx *ctx, const BSL_Param *param, const char *otp, const uint32_t otpLen,uint64_t *matched)
{char targetOtp[OTP_MAX_DIGITS];uint32_t targetOtpLen = sizeof(targetOtp);uint64_t movingFactor;int32_t ret = HotpGen(ctx, param, targetOtp, &targetOtpLen, &movingFactor);if (ret != HITLS_AUTH_SUCCESS) {BSL_ERR_PUSH_ERROR(ret);return ret;}if (strncmp(otp, targetOtp, otpLen) != 0) {BSL_ERR_PUSH_ERROR(HITLS_AUTH_OTP_VALIDATE_MISMATCH);return HITLS_AUTH_OTP_VALIDATE_MISMATCH;}if (matched != NULL) {*matched = movingFactor;}return HITLS_AUTH_SUCCESS;
}int32_t TotpValidate(HITLS_AUTH_OtpCtx *ctx, const BSL_Param *param, const char *otp, const uint32_t otpLen,uint64_t *matched)
{int32_t ret;char targetOtp[OTP_MAX_DIGITS];uint32_t targetOtpLen = sizeof(targetOtp);uint32_t validWindow = ((TotpCtx *)ctx->ctx)->validWindow;uint64_t movingFactor;for (int64_t offset = -(int64_t)validWindow; offset < (int64_t)validWindow + 1; offset++) {ret = TotpGen(ctx, param, offset, targetOtp, &targetOtpLen, &movingFactor);if (ret != HITLS_AUTH_SUCCESS) {BSL_ERR_PUSH_ERROR(ret);return ret;}if (strncmp(otp, targetOtp, otpLen) == 0) {if (matched != NULL) {*matched = movingFactor;}return HITLS_AUTH_SUCCESS;}}BSL_ERR_PUSH_ERROR(HITLS_AUTH_OTP_VALIDATE_MISMATCH);return HITLS_AUTH_OTP_VALIDATE_MISMATCH;
}int32_t HITLS_AUTH_OtpValidate(HITLS_AUTH_OtpCtx *ctx, const BSL_Param *param, const char *otp, const uint32_t otpLen,uint64_t *matched)
{if (ctx == NULL || ctx->key.data == NULL || param == NULL || otp == NULL || otpLen == 0) {BSL_ERR_PUSH_ERROR(HITLS_AUTH_OTP_INVALID_INPUT);return HITLS_AUTH_OTP_INVALID_INPUT;}if (otpLen != ctx->digits) {BSL_ERR_PUSH_ERROR(HITLS_AUTH_OTP_VALIDATE_MISMATCH);return HITLS_AUTH_OTP_VALIDATE_MISMATCH;}switch (ctx->protocolType) {case HITLS_AUTH_OTP_HOTP:return HotpValidate(ctx, param, otp, otpLen, matched);case HITLS_AUTH_OTP_TOTP:return TotpValidate(ctx, param, otp, otpLen, matched);default:BSL_ERR_PUSH_ERROR(HITLS_AUTH_OTP_INVALID_PROTOCOL_TYPE);return HITLS_AUTH_OTP_INVALID_PROTOCOL_TYPE;}
}
