Python TOTP 算法
Time-based One-Time Password(TOTP)
是一种基于时间的一次性密码算法,用于增强身份验证的安全性。
TOTP
基于HMAC(Hash-based Message Authentication Code)
算法和时间戳生成一次性密码。用户和服务器之间共享一个密钥,在每个时间步长(通常是30
秒),基于当前时间戳和共享密钥,使用HMAC
算法生成一个哈希值。然后从哈希值中提取一个固定长度的动态密码。这个动态密码在设定的时间步长内有效,之后会自动失效。
用户在进行身份验证时,需要输入当前时间步长内生成的动态密码。服务器会使用相同的算法和共享密钥,验证用户提供的密码是否匹配。由于动态密码在时间步长过期后就会失效,即使被截获,也无法在下一个时间步长内重复使用。
TOTP
广泛应用于双因素身份验证(2FA)
和多因素身份验证(MFA)
的实现中。通过结合用户的密码和每次生成的动态密码,TOTP
提供了一层额外的安全保护,有效降低了密码被盗用或猜测的风险。
常见的TOTP
应用包括Google Authenticator
和Authy
等身份验证应用程序,它们生成基于TOTP
算法的动态密码,并与用户的在线账户相绑定,提供更安全的登录方式。
Python
实现
下面是我使用FastApi
实现的TOTP
算法的完整测试代码:
'''
Author: matiastang
Date: 2023-10-31 11:30:12
LastEditors: matiastang
LastEditTime: 2024-03-14 13:51:14
FilePath: /mt-fastapi/app/router/totp/totp_views.py
Description: TOTP认证
'''
import pyotp
import base64
from pydantic import BaseModel, Field, constr
from fastapi import APIRouter
from app.types import response
# 路由
router = APIRouter(
prefix="/totp",
tags=["totp"],
)
# 类型
class TOTPPathBody(BaseModel):
issuer: str = Field(min_length=1)
user: str = Field(min_length=1)
key: str = Field(min_length=8)
class TOTPCodeBody(BaseModel):
key: str = Field(min_length=8)
#接口
@router.post('/otpauth')
def generate_totp_path(body: TOTPPathBody) -> response.HttpResponse:
# normal_key = 'matiastang18380449615'
# 生成secret
# 注意如果secret_key有补=的情况,则不需要带上末尾的=
# 当key%8=0时,不会出现补=的情况。
secret_key = base64.b32encode(body.key.encode("utf-8")).decode("utf-8")
# 生成otpauth
'''
otpauth的默认格式为:
otpauth://totp/{label}?secret={secret}&issuer={issuer}
secret为秘钥的base32编码
issuer为发行人或公司
label为用户信息,但是一些工具如:Google Authenticator,中将label显示为标题
如果lable只等于用户名的话,添加后,将无法判断是哪个公司的,这样会有个问题,无法直观的判断是那个公司的验证码,所以一般使用label = f'{body.issuer}:{body.user}'
'''
label = f'{body.issuer}:{body.user}'
url = f'otpauth://totp/{label}?secret={secret_key}&issuer={body.issuer}'
return response.ResponseSuccess(url)
@router.get('/otpauth')
def get_generate_totp_path(issuer: str, user: str, key: str):
body = TOTPPathBody(issuer=issuer, user=user, key=key)
return generate_totp_path(body)
@router.post('/totpcode')
def totp_code(body: TOTPCodeBody) -> response.HttpResponse:
# normal_key = 'matiastang18380449615'
# 生成secret
secret_key = base64.b32encode(body.key.encode("utf-8")).decode("utf-8")
# 使用密钥和时间间隔(默认为 30 秒)创建一个 TOTP 对象
totp = pyotp.TOTP(secret_key)
# 生成当前的 OTP
current_otp = totp.now()
# print(f"当前OTP: {current_otp}")
return response.ResponseSuccess(current_otp)
@router.get('/totpcode')
def get_totp_code(key: str):
body = TOTPCodeBody(key=key)
return totp_code(body)
@router.get('/current')
# def current_totp(body: TOTPBody) -> response.HttpResponse:
def current_totp() -> response.HttpResponse:
# 设置服务端密钥
secret_key = base64.b32encode('matiastang18380449615'.encode("utf-8"))
print('secret_key: ', secret_key)
# title = "TDYTECH"
# name = 'matiastang'
# QRURL = f'otpauth://totp/{title}:{name}?secret={secret_key}&issuer={title}'
# 'otpauth://totp/TDY:matiastang?secret=18380449615&issuer=TDY'
# otpauth://totp/{label}?secret={secret}&issuer={issuer}
# label 可以填写用户的名字,secret 就是上文中经过 Base32 编码后的密钥,issuer 代表应用名,比如 Google。
# 一个完整的示例如下:otpauth://totp/Passkou?secret=6shyg3uens2sh5slhey3dmh47skvgq5y&issuer=Test
# otpauth://totp/Passkou?secret=6shyg3uens2sh5slhey3dmh47skvgq5y&issuer=Test
# otpauth://totp/TDY:matiastang?secret=NVQXI2LBON2GC3THGE4DGOBQGQ2DSNRRGU======&issuer=TDY
# otpauth://totp/TDY:matiastang?secret=NVQXI2LBON2GC3THGE4DGOBQGQ2DSNRRGU&issuer=TDY
# 注意如果secret_key有补=的情况,则不需要带上末尾的=
# 当key%8=0时,不会出现补=的情况。
# 使用密钥和时间间隔(默认为 30 秒)创建一个 TOTP 对象
totp = pyotp.TOTP(secret_key)
# 生成当前的 OTP
current_otp = totp.now()
print(f"当前OTP: {current_otp}")
return response.ResponseSuccess(current_otp)
上面的代码有点儿乱,用于测试的,后面真正使用的时候再简单整理一下,再同步更新。