TOTP算法原理与实现

article/2025/8/19 3:15:00

一、原理说明

在这里插入图片描述

基于 HMAC-SHA1 生成的 20 字节哈希值,通过 ​偏移量截取法​ 生成 32 位整数示例:

假设 HMAC-SHA1 生成的 20 字节哈希值为:

1f 86 98 69 0e 02 ca 16 61 85 50 ef 7f 19 da 8e 94 5b 55 5a(字节编号从 0 到 19,每个字节用十六进制表示)

步骤分解​

​1、提取最后一个字节​

第 19 个字节(最后一个字节)的值为 ​0x5A(十六进制):

1f 86 98 69 0e 02 ca 16 61 85 50 ef 7f 19 da 8e 94 5b 55 5a

​2、计算偏移量(Offset)​​

取 0x5A 的 ​低 4 位​(即 0xA,十进制为 10)作为偏移量:

1f 86 98 69 0e 02 ca 16 61 85 50 ef 7f 19 da 8e 94 5b 55 5a

3、​截取 4 字节数据​

从偏移量 10 开始,截取连续的 4 个字节:

1f 86 98 69 0e 02 ca 16 61 85 50 ef 7f 19 da 8e 94 5b 55 5a

4、转为32位整数

二进制形式:
01010000 11101111 01111111 00011001
对应十六进制:0x50ef7f19
十进制值:1,359,102,489

5、​生成 6 位 OTP​

1,359,102,489 % 1,000,000 = 102,489

最终验证码:102489

二、代码示例

import base64
import hashlib
import hmac
import struct
import time
import os
from typing import Uniondef generate_secret_key(length: int = 16) -> str:"""生成安全的 Base32 编码密钥"""random_bytes = os.urandom(length)secret = base64.b32encode(random_bytes).decode('utf-8')return secret.rstrip('=')  # 去除填充等号(兼容 Google Authenticator)def hotp(secret: str, counter: int, digits: int = 6) -> str:"""基于 HMAC-SHA1 的 HOTP 算法实现"""key = base64.b32decode(secret.upper() + '=' * ((8 - len(secret) % 8) % 8))msg = struct.pack('>Q', counter)hmac_hash = hmac.new(key, msg, hashlib.sha1).digest()# 动态截断处理offset = hmac_hash[-1] & 0x0Fbinary_code = struct.unpack('>I', hmac_hash[offset:offset + 4])[0] & 0x7FFFFFFFreturn str(binary_code % 10 ** digits).zfill(digits)def totp(secret: str, time_step: int = 30, digits: int = 6) -> str:"""生成当前时间的 TOTP 验证码"""counter = int(time.time() // time_step)return hotp(secret, counter, digits)def verify_totp(secret: str, code: str, time_window: int = 1) -> bool:"""验证 TOTP 码(允许时间窗口容错)"""current_counter = int(time.time() // 30)for i in range(-time_window, time_window + 1):expected_code = hotp(secret, current_counter + i)if hmac.compare_digest(expected_code, code):return Truereturn False# ------------------- 示例用法 -------------------
if __name__ == "__main__":# 1. 生成密钥secret_key = generate_secret_key()print(f"生成的密钥: {secret_key}")  # 示例输出: JBSWY3DPEHPK3PXP# 2. 生成当前 TOTP 验证码current_otp = totp(secret_key)print(f"当前验证码: {current_otp}")  # 示例输出: 123456# 3. 验证用户输入user_input = input("请输入验证码: ")if verify_totp(secret_key, user_input):print("验证成功!")else:print("验证失败!")

三、如何查看Google Authenticator中的密钥

Google Authenticator在手机上可以导出,导出后是加密的迁移二维码​(格式:otpauth-migration://offline?data=…)

通过迁移二维码获取加密数据后,可使用开源工具(如 google-authenticator-exporter)解析二维码链接中的 totpSecret 字段,直接提取密钥

# 安装工具
git clone https://github.com/krissrex/google-authenticator-exporter.git
cd google-authenticator-exporter
npm install
npm run start
# 输入导出的迁移链接,即可解析出密钥

如果单纯的想生成,则可以使用一下代码:

import base64
import hashlib
import hmac
import struct
import timedef hotp(secret: str, counter: int, digits: int = 6) -> str:key = base64.b32decode(secret.upper() + '=' * ((8 - len(secret) % 8) % 8))msg = struct.pack('>Q', counter)hmac_hash = hmac.new(key, msg, hashlib.sha1).digest()# 动态截断处理offset = hmac_hash[-1] & 0x0Fbinary_code = struct.unpack('>I', hmac_hash[offset:offset + 4])[0] & 0x7FFFFFFFreturn str(binary_code % 10 ** digits).zfill(digits)def totp(secret: str, time_step: int = 30, digits: int = 6) -> str:counter = int(time.time() // time_step)return hotp(secret, counter, digits)if __name__ == "__main__":current_otp = totp("33FSBRWL36KGTEJUGRIHCLMSYEL4HESR")print(current_otp)

如果想计算剩余时间:则用 30 - 时间戳 % 30

def totp(secret: str, time_step: int = 30, digits: int = 6) -> str:t = time.time()remain = time_step - t % time_stepcounter = int(t // time_step)return hotp(secret, counter, digits), remain

四、自制客户端

我们可以做成自己的客户端,为了简单,可以做成HTML来实现,不仅轻量,而且跨平台:

以下代码可以直接使用,需要替换的就只有JSON数据片段,换成自己的就可以了:

        // TOTP数据const totpData = [{"name": "双因子验证","issuer": "测试","algorithm": "SHA1","digits": "SIX","type": "TOTP","totpSecret": "SDHLINVCJFE35TNKISYP7WKA5TYFUS2B"}];

效果如下:
在这里插入图片描述

完整代码:

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>TOTP验证码生成器</title><style>/* 原有样式保持不变 */* {box-sizing: border-box;margin: 0;padding: 0;font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;}body {background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);color: #fff;min-height: 100vh;padding: 20px;}.container {max-width: 900px;margin: 0 auto;background: rgba(0, 0, 0, 0.7);border-radius: 15px;padding: 30px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);backdrop-filter: blur(10px);}header {text-align: center;margin-bottom: 30px;padding-bottom: 20px;border-bottom: 2px solid rgba(255, 255, 255, 0.1);}h1 {font-size: 2.5rem;margin-bottom: 10px;background: linear-gradient(to right, #ff7e5f, #feb47b);-webkit-background-clip: text;-webkit-text-fill-color: transparent;text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);}.subtitle {color: #ccc;font-size: 1.1rem;margin-bottom: 20px;}.stats {display: flex;justify-content: space-around;background: rgba(255, 255, 255, 0.1);border-radius: 10px;padding: 15px;margin: 20px 0;}.stat-item {text-align: center;}.stat-value {font-size: 1.8rem;font-weight: bold;color: #4facfe;}.stat-label {font-size: 0.9rem;color: #aaa;}.totp-grid {display: grid;grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));gap: 20px;margin-top: 30px;}.totp-card {background: rgba(30, 30, 46, 0.8);border-radius: 12px;padding: 20px;transition: transform 0.3s, box-shadow 0.3s;position: relative;overflow: hidden;border: 1px solid rgba(255, 255, 255, 0.1);}.totp-card:hover {transform: translateY(-5px);box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);background: rgba(40, 40, 56, 0.9);}.issuer {font-size: 1.3rem;font-weight: bold;margin-bottom: 5px;color: #4facfe;}.name {color: #aaa;font-size: 0.9rem;margin-bottom: 15px;}.code-container {display: flex;align-items: center;justify-content: space-between;margin: 15px 0;}.code {font-size: 2.2rem;font-weight: bold;letter-spacing: 5px;text-align: center;color: #fff;background: rgba(0, 0, 0, 0.3);padding: 10px;border-radius: 8px;font-family: monospace;flex: 1;margin-right: 10px;}.copy-btn {padding: 12px 15px;background: #4facfe;color: white;border: none;border-radius: 8px;cursor: pointer;font-weight: bold;transition: all 0.3s;white-space: nowrap;}.copy-btn:hover {background: #00f2fe;transform: translateY(-2px);box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);}.copy-btn.copied {background: #4CAF50;animation: pulse 0.5s;}@keyframes pulse {0% { transform: scale(1); }50% { transform: scale(1.05); }100% { transform: scale(1); }}.timer {height: 6px;background: rgba(255, 255, 255, 0.1);border-radius: 3px;overflow: hidden;margin-top: 15px;}.timer-bar {height: 100%;background: linear-gradient(to right, #4facfe, #00f2fe);width: 100%;transition: width 1s linear;}.time-left {text-align: right;font-size: 0.9rem;color: #aaa;margin-top: 5px;}.footer {text-align: center;margin-top: 30px;padding-top: 20px;border-top: 2px solid rgba(255, 255, 255, 0.1);color: #aaa;font-size: 0.9rem;}.copy-feedback {position: fixed;top: 20px;left: 50%;transform: translateX(-50%);background: rgba(76, 175, 80, 0.9);color: white;padding: 10px 20px;border-radius: 5px;z-index: 1000;opacity: 0;transition: opacity 0.3s;}.copy-feedback.show {opacity: 1;}@media (max-width: 768px) {.container {padding: 15px;}.totp-grid {grid-template-columns: 1fr;}h1 {font-size: 2rem;}}</style>
</head>
<body><div class="container"><header><h1>TOTP验证码生成器</h1><p class="subtitle">基于时间的一次性密码生成工具</p></header><div class="stats"><div class="stat-item"><div class="stat-value" id="total-count">11</div><div class="stat-label">总账户数</div></div><div class="stat-item"><div class="stat-value" id="update-time">30</div><div class="stat-label">刷新倒计时</div></div><div class="stat-item"><div class="stat-value" id="current-time">00:00:00</div><div class="stat-label">当前时间</div></div></div><div class="totp-grid" id="totp-container"><!-- TOTP卡片将通过JavaScript动态生成 --></div><div class="footer"><p>基于HMAC-SHA1算法 | 每30秒自动刷新 | 安全验证工具</p></div></div><!-- 复制反馈提示 --><div class="copy-feedback" id="copy-feedback">已复制到剪贴板!</div><script>// Base32解码函数function base32Decode(input) {const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";let output = new Uint8Array(Math.ceil(input.length * 5 / 8));let buffer = 0;let bitsLeft = 0;let count = 0;for (let i = 0; i < input.length; i++) {const char = input[i].toUpperCase();if (char === '=' || char === ' ') continue;const index = alphabet.indexOf(char);if (index === -1) continue;buffer = (buffer << 5) | index;bitsLeft += 5;if (bitsLeft >= 8) {output[count++] = (buffer >>> (bitsLeft - 8)) & 0xFF;bitsLeft -= 8;}}return output.slice(0, count);}// 动态截断函数function dynamicTruncation(hmac) {const offset = hmac[hmac.length - 1] & 0x0F;return (((hmac[offset] & 0x7F) << 24) |((hmac[offset + 1] & 0xFF) << 16) |((hmac[offset + 2] & 0xFF) << 8) |(hmac[offset + 3] & 0xFF));}// 生成HOTPfunction hotp(secret, counter, digits = 6) {const key = base32Decode(secret);const msg = new Uint8Array(8);for (let i = 7; i >= 0; i--) {msg[i] = counter & 0xFF;counter >>= 8;}// 使用Web Crypto API进行HMAC-SHA1计算return crypto.subtle.importKey("raw", key, { name: "HMAC", hash: "SHA-1" }, false, ["sign"]).then(cryptoKey => {return crypto.subtle.sign("HMAC", cryptoKey, msg);}).then(signature => {const hmac = new Uint8Array(signature);const code = dynamicTruncation(hmac) % Math.pow(10, digits);return code.toString().padStart(digits, '0');});}// 生成TOTPfunction totp(secret, timeStep = 30, digits = 6) {const t = Math.floor(Date.now() / 1000);const remain = timeStep - (t % timeStep);const counter = Math.floor(t / timeStep);return hotp(secret, counter, digits).then(code => {return { code, remain };});}// TOTP数据const totpData = [{"name": "双因子验证","issuer": "测试","algorithm": "SHA1","digits": "SIX","type": "TOTP","totpSecret": "SDHLINVCJFE35TNKISYP7WKA5TYFUS2B"}];// 存储卡片DOM引用的数组let totpCards = [];// 初始渲染所有卡片function renderInitialCards() {const container = document.getElementById('totp-container');container.innerHTML = '';totpCards = totpData.map(item => {const card = document.createElement('div');card.className = 'totp-card';card.innerHTML = `<div class="issuer">${item.issuer}</div><div class="name">${item.name}</div><div class="code-container"><div class="code">正在生成...</div><button class="copy-btn">复制</button></div><div class="timer"><div class="timer-bar" style="width: 100%"></div></div><div class="time-left">剩余时间: 30秒</div>`;container.appendChild(card);// 返回需要更新的元素return {element: card,codeElement: card.querySelector('.code'),timerBar: card.querySelector('.timer-bar'),timeLeft: card.querySelector('.time-left'),copyButton: card.querySelector('.copy-btn')};});// 初始化复制按钮setupCopyButtons();}// 更新单个卡片async function updateCard(card, item) {try {const { code, remain } = await totp(item.totpSecret);const percent = (remain / 30) * 100;card.codeElement.textContent = code;card.timerBar.style.width = `${percent}%`;card.timeLeft.textContent = `剩余时间: ${remain}`;} catch (error) {console.error(`生成TOTP时出错: ${item.issuer} - ${item.name}`, error);card.codeElement.textContent = 'ERROR';card.timeLeft.textContent = '生成验证码失败';}}// 更新所有卡片async function updateAllCards() {const updatePromises = totpData.map((item, index) => {return updateCard(totpCards[index], item);});await Promise.all(updatePromises);// 更新时间显示const now = new Date();const timeString = now.toTimeString().split(' ')[0];document.getElementById('current-time').textContent = timeString;// 更新刷新倒计时const currentSeconds = Math.floor(Date.now() / 1000);const refreshTime = 30 - (currentSeconds % 30);document.getElementById('update-time').textContent = refreshTime;}// 复制功能function setupCopyButtons() {const copyButtons = document.querySelectorAll('.copy-btn');const feedback = document.getElementById('copy-feedback');copyButtons.forEach(button => {button.addEventListener('click', function() {const code = this.previousElementSibling.textContent;copyToClipboard(code);// 显示反馈动画this.classList.add('copied');feedback.classList.add('show');setTimeout(() => {this.classList.remove('copied');feedback.classList.remove('show');}, 2000);});});}// 复制到剪贴板函数function copyToClipboard(text) {// 使用现代Clipboard API if (navigator.clipboard) {navigator.clipboard.writeText(text).catch(err => {console.error('复制失败:', err);fallbackCopyText(text);});} else {// 兼容旧浏览器的备用方案 fallbackCopyText(text);}}// 兼容旧浏览器的复制方法function fallbackCopyText(text) {const textArea = document.createElement('textarea');textArea.value = text;textArea.style.position = 'fixed';textArea.style.top = '-1000px';textArea.style.left = '-1000px';document.body.appendChild(textArea);textArea.select();try {document.execCommand('copy');} catch (err) {console.error('备用复制方法失败:', err);}document.body.removeChild(textArea);}// 页面加载时初始化document.addEventListener('DOMContentLoaded', async () => {// 初始渲染卡片renderInitialCards();// 初始更新所有卡片await updateAllCards();// 每秒更新一次setInterval(updateAllCards, 1000);});</script>
</body>
</html>

http://www.hkcw.cn/article/RffuDZCmtv.shtml

相关文章

JAVA与C语言之间的差异(二)

一、while循环&#xff0c;do while循环 众所周知&#xff0c;while循环的结构是这样的&#xff1a; while(循环条件) {循环语句;} 在C语言中&#xff0c;可以直接再循环条件处写1&#xff0c;表示死循环&#xff0c;直到执行break才可以跳出&#xff0c;但是在JAVA中不可以&…

环境温度通过H2A.Zub和H3K27me3动态调控拟南芥细胞命运决定

2025年4月22日&#xff0c;中国科学院遗传与发育生物学研究所肖军研究组在Developmental Cell在线发表了题为Dynamic control of H2A.Zub and H3K27me3 by ambient temperature during cell fate determination in Arabidopsis的研究论文&#xff0c;本研究综合运用ChIP-seq、C…

国际调解院正式落户香港有哪些亮点 凝聚国际人才

国际调解院将在香港成立,计划于5月30日在港举行《关于建立国际调解院的公约》签署仪式。在香港特区立法会行政长官互动交流答问会上,李家超表示,国际调解院在港设立对香港多方面有利,认为其影响深远,可不断发挥影响力,凝聚不同国家和地区的人才。他表示,特区政府愿意将人…

特朗普关税政策为何被暂时恢复 法院裁决引发争议

5月29日,美国联邦巡回上诉法院批准了特朗普政府的请求,暂时搁置了美国国际贸易法院此前做出的禁止执行依据《国际紧急经济权力法》对多国加征关税措施的裁决。联邦巡回上诉法院在裁决书中表示,在审议相关动议文件期间,美国国际贸易法院在这些案件中作出的判决和永久性禁令将…

中国寻亲网将关闭 负责人回应原因 公司注销不影响运营

中国寻亲网将关闭 负责人回应原因 公司注销不影响运营。近日,中国寻亲网在官方网站发布公告,宣布将于2025年7月15日正式关闭服务器。自5月1日起,该网站已停止发布新的寻亲信息,仅保留原有数据的修改功能。这一消息引起众多网友关注,并引发对关闭原因的猜测。寻子家长、电影…

大众中国CEO:中国人喜欢智能 欧洲人看中实用 市场偏好差异显著

大众中国CEO:中国人喜欢智能 欧洲人看中实用 市场偏好差异显著。大众中国CEO贝瑞德指出,中国消费者和欧洲消费者在电动汽车的偏好上存在显著差异。中国年轻用户群体对“智能座舱”和“语音交互”功能习以为常,电动车主的平均年龄不到35岁,他们崇尚数字体验。相比之下,欧洲…

C库-进程

库 头文件: #include<stdio.h> <>代表区系统路径下查找头文件 /usr/include #include"head.h" ""代表先去当前路径下查找头文件&#xff0c;找不到再去系统路径下查找 头文件也就是以.h结尾的文件&#xff0c;其中包含&#xff1a;宏定义…

华为OD机试真题——阿里巴巴找黄金宝箱Ⅰ(2025A卷:100分)Java/python/JavaScript/C/C++/GO最佳实现

2025 A卷 100分 题型 本专栏内全部题目均提供Java、python、JavaScript、C、C++、GO六种语言的最佳实现方式; 并且每种语言均涵盖详细的问题分析、解题思路、代码实现、代码详解、3个测试用例以及综合分析; 本文收录于专栏:《2025华为OD真题目录+全流程解析+备考攻略+经验分…

泡泡玛特成基金重仓新贵 消费投资聚焦“含新量”

泡泡玛特成基金重仓新贵 消费投资聚焦“含新量”!随着泡泡玛特在港交所挂牌并凭借爆款IP实现股价大幅上涨,不少曾经重仓持有贵州茅台的基金经理开始转向投资泡泡玛特。许多基金经理也在积极寻找能够复制泡泡玛特成功的新消费标的。一位长期关注新消费领域的公募基金经理表示,…

刘扬伟:富士康即将宣布第二家日本车企合作伙伴 电动车业务再扩展

刘扬伟:富士康即将宣布第二家日本车企合作伙伴 电动车业务再扩展!富士康董事长刘扬伟在股东大会上宣布,公司即将与第二家日本汽车制造商建立合作关系,继续拓展电动车业务。他提到两家日本车厂中,一家已经公布,另一家也快了,但未透露更多细节。本月早些时候,富士康旗下的…

美若禁止对华出售EDA对我国有何影响 芯片设计工具受限

美若禁止对华出售EDA对我国有何影响 芯片设计工具受限!经历两天传闻后,两家美国芯片EDA大厂Synopsys(新思科技)和Cadence(楷登电子)确认,美国商务部工业和安全局(BIS)要求它们对中国企业断供芯片设计EDA软件工具。Synopsys在5月29日发布公告称,公司收到了BIS的信函,…

女子称坐飞机万元金手链托运后丢失 行李完好首饰不翼而飞

5月26日,杨女士在社交平台发布视频称,她在25日搭乘春秋航空公司的航班从西安返回宁波。落地后发现托运行李中一条价值一万两千余元的金手链不见了。奇怪的是,该金手链的内外包装完好无损。目前,宁波市公安局机场分局已接到杨女士的报警,并受理此案。杨女士表示,她原本不打…

网飞回应苦尽柑来遇见你霸凌风波 剧组工作方式受质疑

网飞(Netflix)热播韩剧《苦尽柑来遇见你》近日被质疑剧组工作人员压榨群演,霸凌风波持续发酵。韩国网络上关于该剧拍摄现场工作方式严苛的爆料不断涌现,爆料人疑似为群演或外包公司员工。有人控诉剧组不愿在非主演身上花钱,不允许群演穿保暖内衣和使用取暖设备,寒冬时节放…

亲妈拿走孩子80多万买房再婚被起诉 法院:全额返还并支付利息

5月29日,南通中院通报了一起典型案例。女子丁某离婚时约定,儿子小雷(化名)随丁某共同生活,男方给付小雷生活费70万元。同时约定这笔钱及长辈给的13.8万元,应作为小雷购买某房产的产权份额。后来,丁某签订房屋买卖合同,并陆续支付房款83.8万元。同年,丁某与汪某登记结婚…

动车弓网检测系统助力铁路运行安全

动车弓网检测是铁路运营中至关重要的环节&#xff0c;其重要性不言而喻&#xff0c;弓网检测可以保障行车安全&#xff0c; 提升运行效率。 检测重点内容 接触网&#xff1a;导线高度、拉出值、磨损、悬挂部件状态。 受电弓&#xff1a;碳滑板厚度、动态接触压力、框架变形。…

部分机票低于1.3折 错峰出游正当时

部分机票低于1.3折 错峰出游正当时。近期全国多地机票价格明显下降,业内人士表示暑假前是错峰出游的好时机。有网友兴奋地表示要马上出发。近日,在一些旅游门店观察到,许多市民正计划利用淡季出行。未来一个月内,从广州飞往昆明、上海、南京、武汉等多个热门旅游城市的机票…

litctf2025复现

[LitCTF 2025]nest_js 开始是一个登录界面&#xff0c;随便输入发现没回显&#xff0c;抓包看看&#xff0c;没看出来什么&#xff0c;猜一下账号是admin直接用常用密码字典爆破 得到密码是password&#xff0c;登录就有flag [LitCTF 2025]test_your_nc 进去就叫我们输入指令…

【QQ音乐】sign签名| data参数加密 | AES-GCM加密 | webpack (下)

1.目标 网址&#xff1a;https://y.qq.com/n/ryqq/toplist/26 我们知道了 sign P(n.data)&#xff0c;其中n.data是明文的请求参数 2.webpack生成data加密参数 那么 L(n.data)就是密文的请求参数。返回一个Promise {<pending>}&#xff0c;所以L(n.data) 是一个异步函数…

MySql(五)

目录 修改表 1--修改表中列的 数据类型 或长度 &#xff08;Modify&#xff09; 语法 格式&#xff1a; 对student的中的 student_info 字段进行修改 1....修改字段长度 2....修改字段类型 2--修改表中的列名&#xff08;change&#xff09; 语法格式&#xff1a; 修改列名 3.删…

C++_核心编程_ 左移运算符重载 “<<” 左移运算符

作用&#xff1a;可以输出自定义数据类型 */ //目标 调用p1,输出Person 中的属性 m_A ,m_B &#xff1a; /* #### 4.5.2 左移运算符重载 “<<” 左移运算符 作用&#xff1a;可以输出自定义数据类型 *///目标 调用p1,输出Person 中的属性 m_A ,m_B &#xff1a; class…