Skip to content

Python TOTP 算法

约 1051 字大约 4 分钟

PythonTOTP

2025-01-20

Time-based One-Time Password(TOTP)是一种基于时间的一次性密码算法,用于增强身份验证的安全性。

TOTP基于HMAC(Hash-based Message Authentication Code)算法和时间戳生成一次性密码。用户和服务器之间共享一个密钥,在每个时间步长(通常是30秒),基于当前时间戳和共享密钥,使用HMAC算法生成一个哈希值。然后从哈希值中提取一个固定长度的动态密码。这个动态密码在设定的时间步长内有效,之后会自动失效。

用户在进行身份验证时,需要输入当前时间步长内生成的动态密码。服务器会使用相同的算法和共享密钥,验证用户提供的密码是否匹配。由于动态密码在时间步长过期后就会失效,即使被截获,也无法在下一个时间步长内重复使用。

TOTP广泛应用于双因素身份验证(2FA)和多因素身份验证(MFA)的实现中。通过结合用户的密码和每次生成的动态密码,TOTP提供了一层额外的安全保护,有效降低了密码被盗用或猜测的风险。

常见的TOTP应用包括Google AuthenticatorAuthy等身份验证应用程序,它们生成基于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)

上面的代码有点儿乱,用于测试的,后面真正使用的时候再简单整理一下,再同步更新。

参考

RFC4226Base 32NodeJSpython&go