← 返回

Telegram MFA Bot:把 2FA/TOTP 验证码放进自己的机器人里(离线计算 + 本地存储)

日常开发/运维里,2FA(双重验证)几乎已经是标配:GitHub、Google、AWS、Cloudflare、各类后台……账号多了以后,验证码查找反而变成了一个高频“打断流”动作:打开手机 → 找到对应 App → 等刷新 → 复制粘贴。

于是我做了一个小工具:一个只给自己用的 Telegram Bot,用来离线计算 TOTP 验证码,并且做成“菜单化”的账号管理器:点选账号即可显示当前 2FA 码,支持刷新/重命名/删除/导入。

缘由:为什么要做一个 Bot 来管 MFA

  • 减少切换成本:很多时候电脑在手、手机不在手(或解锁麻烦),但登录动作又必须要验证码。
  • 多账号管理:同一个平台多个身份(工作/个人/客户),在 Authenticator 里也会很长。
  • 可自托管:TOTP 计算本质是本地算法,不依赖外部接口;用 Bot 只是把“展示界面”换成消息交互。

备注:这个项目的定位是“自己掌控运行环境”的个人工具,不是公用服务(安全注意事项见文末)。

使用场景

  • 远程登录/运维:在服务器上操作时,需要临时拿到验证码(前提是 bot 运行在你可信的机器上)。
  • 多平台/多租户:几十个账号时,通过菜单快速定位,不用在手机里翻。
  • 导入迁移:已有的密钥(例如导出表格)可以一键导入到 SQLite。

最终效果(交互体验)

  • 发送 /start 后出现主菜单:
    • 查看账号列表(单列带序号,长名称会自动缩略)
    • 手动添加账号(两步对话:备注名 → Secret)
    • 批量导入(从项目目录的 mfa_secrets_export.xlsx 导入)
  • 点选某个账号:
    • 显示当前验证码(6 位自动加空格,便于肉眼核对)
    • 显示剩余有效期进度条(30 秒制)
    • 按钮:刷新 / 重命名 / 删除 / 返回列表

(这里你可以在 Hexo 里插入两张截图:主菜单 & 验证码详情页)

实现方式:代码结构与关键点

项目很小,核心就两层:

  • bot.py:Telegram 交互层(菜单、回调、对话状态机、权限控制)
  • mfa_manager.py:数据与算法层(SQLite 持久化、Excel 导入、pyotp 计算 TOTP)

下面摘几段关键实现(为便于阅读做了少量裁剪):

0) 入口与权限:只给自己用

通过 OWNER_ID 做最小权限控制:只有指定 Telegram 用户才能操作菜单。

1
2
3
4
5
6
7
8
9
10
11
# bot.py(节选)
OWNER_ID = int(os.getenv("OWNER_ID", "0"))

def is_owner(update):
return OWNER_ID == 0 or update.effective_user.id == OWNER_ID

def start(update, context):
if not is_owner(update):
update.message.reply_text("⚠️ 无访问权限")
return
# ... 渲染欢迎菜单 ...

1) TOTP 计算:完全离线

TOTP 的本质是共享密钥 + 时间窗口(通常 30 秒)生成动态码。项目使用 pyotp 来做算法实现:

1
2
3
import pyotp
totp = pyotp.TOTP(secret)
code = totp.now()

因此运行时不需要调用任何第三方 API —— 只要机器时间相对准确即可。

项目里封装成方法,顺便把用户输入里常见的空格去掉:

1
2
3
4
# mfa_manager.py(节选)
def generate_totp(self, secret):
totp = pyotp.TOTP(secret.strip().replace(" ", ""))
return totp.now()

2) SQLite 存储:轻量、够用

所有账号密钥存进本地 mfa_data.db,表结构大致是:

  • account:账号/名称(用于展示或备注默认值)
  • issuer:发行方(导入时可带)
  • secret:TOTP 密钥(Base32)
  • remark:用户自定义备注(菜单里优先显示)
  • created_at:创建时间

这意味着 不会丢在聊天记录里;但也带来一个重要前提:运行机器的磁盘必须可信(后面会说风险)。

初始化表结构的代码如下:

1
2
3
4
5
6
7
8
9
10
11
# mfa_manager.py(节选)
cursor.execute("""
CREATE TABLE IF NOT EXISTS mfa_secrets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account TEXT NOT NULL,
issuer TEXT,
secret TEXT NOT NULL,
remark TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")

3) Bot 交互:InlineKeyboard + CallbackQuery

菜单体验依赖 Telegram 的 Inline Keyboard:

  • 账号列表:每个按钮的 callback_data 形如 show_<id>,点击后查询 DB → 生成验证码 → 编辑原消息展示。
  • 刷新:按钮还是同一个 show_<id>,点击即重新计算 totp.now()
  • 重命名/删除:分别走对话状态机和二次确认。

为了避免频繁编辑导致的 “Message is not modified” 报错,封装了一个安全编辑函数:当内容没变化时静默跳过。

1
2
3
4
5
6
7
# bot.py(节选)
def safe_edit(query, text, reply_markup=None):
try:
query.edit_message_text(text, reply_markup=reply_markup, parse_mode='Markdown')
except BadRequest as e:
if "Message is not modified" not in str(e):
logging.error(e)

菜单回调的核心分发逻辑是:识别 callback_data 前缀并路由到对应动作(展示验证码/返回列表/导入/删除确认等):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# bot.py(节选)
def handle_callback(update, context):
query = update.callback_query
if not is_owner(update):
query.answer("无权限")
return

data = query.data
if data.startswith("show_"):
show_code(update, context)
elif data == "main_menu":
show_main_menu(update, context)
elif data == "import":
count = mfa_manager.import_from_excel('mfa_secrets_export.xlsx')
query.answer(f"成功导入 {count} 个账号", show_alert=True)
show_main_menu(update, context)
# ... 其余分支省略 ...

4) 权限控制:只允许 Owner 使用

通过环境变量 OWNER_ID 限制访问:只有指定 Telegram 用户 id 才能使用机器人。没配置(或为 0)时默认放开(更适合本地测试,但不建议线上这么做)。

5) 代理与网络波动:更稳定的轮询

机器人使用 polling 模式,并且:

  • 支持 PROXY_URL 走代理
  • 延长 read_timeout/connect_timeout
  • 统一错误处理:对常见网络波动只打印关键信息

Excel 批量导入格式

机器人会从项目目录读取固定文件名:mfa_secrets_export.xlsx

表头至少需要包含:

  • Account
  • Secret

可选:

  • Issuer

导入时会按 Secret 去重,避免重复插入。

导入逻辑的关键点是:读取表格 → 规范化 Secret → 以 Secret 做去重 → 写入 SQLite:

1
2
3
4
5
6
7
8
9
10
# mfa_manager.py(节选)
df = pd.read_excel(excel_path)
for _, row in df.iterrows():
secret = str(row["Secret"]).strip().replace(" ", "")
cursor.execute("SELECT id FROM mfa_secrets WHERE secret = ?", (secret,))
if not cursor.fetchone():
cursor.execute(
"INSERT INTO mfa_secrets (account, issuer, secret, remark) VALUES (?, ?, ?, ?)",
(str(row["Account"]), str(row.get("Issuer", "")), secret, str(row["Account"])),
)

如何运行(自托管)

1) 准备环境变量

建议在项目根目录放一个 .env(不要提交到仓库):

1
2
3
4
TELEGRAM_BOT_TOKEN=123456:xxxxx
OWNER_ID=123456789
# 可选:代理(例如 socks5 / http 代理,取决于你使用的 telegram 库支持)
PROXY_URL=socks5://127.0.0.1:7890

2) 安装依赖

项目用到的主要依赖:

  • python-telegram-bot(代码风格看起来是 v13 系列:Updater / Filters / use_context=True
  • python-dotenv
  • pyotp
  • pandas(以及 openpyxl 用于读取 xlsx)

安装示例:

1
pip install python-telegram-bot==13.* python-dotenv pyotp pandas openpyxl

3) 启动

1
python bot.py

然后在 Telegram 对你的 Bot 发送 /start 即可。

安全注意事项(务必读)

这个项目能省事,但密钥的安全性永远优先

  • Telegram 聊天不是端到端加密(Bot 会经过 Telegram 服务器),并且你的 Bot Token 一旦泄露,风险极高。
  • SQLite 明文存储 secret:拿到你的磁盘/备份的人就可能拿到所有 2FA 密钥。
  • OWNER_ID 只是应用层限制:能防止“正常使用路径”的别人点进来,但防不住运行机被入侵、Token 泄露等问题。

建议:

  • 只在你完全信任的机器上自托管(例如个人小服务器/家用 NAS,且做好系统加固)。
  • OWNER_ID 必须设置成你的 Telegram id。
  • .envmfa_data.dbmfa_secrets_export.xlsx 做好权限控制与备份策略(备份本身也要加密)。
  • 如果要更进一步:给 secret 做本地加密(例如用主密码派生密钥),并考虑把敏感输入改为一次性会话、避免在聊天里长期留痕。

可以继续完善的方向

  • 加密存储:对 secret 字段加密,主密码只在进程内存中短暂存在。
  • 更友好的导入/导出:支持上传文件导入、导出加密备份。
  • 支持 HOTP / 不同位数 / 不同周期:适配更多平台。
  • 更严格的安全策略:白名单 chat、限制命令、自动超时锁定等。

这个 Bot 的目标很明确:把 TOTP 的“计算”留在你掌控的机器上,把“取码”变成一次点击。如果你准备长期使用,最值得优先投入的是:本地加密存储(保护 secret)、更严格的访问边界(限制 chat/命令)、以及对备份介质的加密与权限控制。