Skip to content

2FA二维码识别工具|安迪小工具

Image 1 Image 1

最近发了一篇关于 Google Authenticator 这种2FA(双重验证)的chrome插件,用二维码来备份密钥和复制。想提取里面的密钥,有人觉得有安全隐患,所以就找AI先写了一个本地工具。

用 Python 写了个小工具,界面简单,功能专一:
只做一件事——识别和解析 Google Authenticator 的二维码图片,提取2FA密钥。

工具亮点

  • 支持普通 otpauth:// 二维码和 Google Authenticator 迁移(otpauth-migration://)二维码
  • 拖图片进来或者点按钮选图片,立刻显示密钥
  • 不联网,纯本地处理,安全放心
  • 没有广告、没有多余功能,界面极简

使用方法

  1. 打开软件,点击"选择二维码图片"
  2. 选中你保存的二维码图片(支持png/jpg/bmp等常见格式)
  3. 稍等几秒,密钥就会显示在下方,可以直接复制

适用场景

  • 备份2FA密钥,防止手机丢失
  • 迁移账号到新设备时,提前保存密钥
  • 想用其它2FA工具时,提取原始密钥

注意事项

  • 本工具仅供个人学习、备份、迁移账号使用,请勿用于非法用途
  • 不会上传任何数据,所有操作都在本地完成
  • 仅支持 Google Authenticator 相关二维码,别的二维码不保证能识别

下载

2FA二维码识别工具 (访问密码: 2199)


有需要的朋友可以自行下载,或者自己动手试试源码!


标签: #效率工具 #2FA #二维码识别 #Google Authenticator #自动化工具 #小而美 #效率优先 #团队管理 #工作效率 #必备工具

源码

import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image
import threading
import os
import re
import base64
import cv2
import zxingcpp
import migration_pb2
import tempfile

class QRCodeScanner:
    def __init__(self, root):
        self.root = root
        self.root.title("2FA 二维码识别工具")
        self.root.geometry("500x500")

        # Configure style
        style = ttk.Style()
        style.configure("TButton", padding=10, font=('Helvetica', 10))
        style.configure("TLabel", font=('Helvetica', 12))

        # Create main frame
        main_frame = ttk.Frame(root, padding="20")
        main_frame.pack(fill=tk.BOTH, expand=True)

        # Title
        title_label = ttk.Label(
            main_frame, 
            text="2FA 二维码识别工具",
            font=('Helvetica', 16, 'bold')
        )
        title_label.pack(pady=20)

        # Select button
        self.select_button = ttk.Button(
            main_frame,
            text="选择二维码图片",
            command=self.select_image
        )
        self.select_button.pack(pady=10)

        # Result text area
        self.result_text = tk.Text(
            main_frame,
            height=10,
            width=50,
            wrap=tk.WORD,
            font=('Helvetica', 10)
        )
        self.result_text.pack(pady=10, fill=tk.BOTH, expand=True)

        # Status label
        self.status_label = ttk.Label(main_frame, text="")
        self.status_label.pack(pady=5)

        # Menu bar with About
        menubar = tk.Menu(self.root)
        helpmenu = tk.Menu(menubar, tearoff=0)
        helpmenu.add_command(label="关于", command=self.show_about)
        menubar.add_cascade(label="帮助", menu=helpmenu)
        self.root.config(menu=menubar)

    def select_image(self):
        file_path = filedialog.askopenfilename(
            filetypes=[
                ("Image files", "*.png *.jpg *.jpeg *.bmp"),
                ("All files", "*.*")
            ]
        )
        if file_path:
            self.process_image(file_path)

    def process_image(self, image_path):
        self.result_text.delete(1.0, tk.END)
        self.result_text.insert(tk.END, "Processing image... Please wait...")
        self.status_label.config(text="Processing...")

        # Run processing in a separate thread
        thread = threading.Thread(
            target=self._process_image_thread,
            args=(image_path,)
        )
        thread.daemon = True
        thread.start()

    def _process_image_thread(self, image_path):
        try:
            # Read the image using OpenCV
            image = cv2.imread(image_path)
            if image is None:
                self._update_ui("Error: Could not read the image file.", error=True)
                return

            # Read and decode QR code
            result = zxingcpp.read_barcode(image)

            if not result:
                self._update_ui("Error: No QR code found in the image.", error=True)
                return

            qr_data = result.text

            # 普通 otpauth:// 逻辑
            if qr_data.startswith('otpauth://'):
                secret_match = re.search(r'secret=([A-Z2-7]+)', qr_data)
                if not secret_match:
                    self._update_ui("Error: Could not find secret key in QR code.", error=True)
                    return
                secret = secret_match.group(1)
                try:
                    base64.b32decode(secret)
                    self._update_ui(f"Successfully extracted 2FA secret:\n{secret}")
                except Exception:
                    self._update_ui("Error: Invalid secret key format.", error=True)
                return
            # otpauth-migration:// 逻辑
            elif qr_data.startswith('otpauth-migration://'):
                import urllib.parse
                parsed = urllib.parse.urlparse(qr_data)
                params = urllib.parse.parse_qs(parsed.query)
                data_b64 = params.get('data', [None])[0]
                if not data_b64:
                    self._update_ui("Error: No data field in migration QR code.", error=True)
                    return
                try:
                    data_bytes = base64.urlsafe_b64decode(data_b64 + '==')
                    payload = migration_pb2.MigrationPayload()
                    payload.ParseFromString(data_bytes)
                    if not payload.otp_parameters:
                        self._update_ui("Error: No OTP accounts found in migration QR code.", error=True)
                        return
                    result_lines = ["检测到 Google Authenticator 迁移二维码,解析结果如下:\n"]
                    for idx, otp in enumerate(payload.otp_parameters, 1):
                        secret = base64.b32encode(otp.secret).decode('utf-8')
                        name = otp.name
                        issuer = otp.issuer
                        result_lines.append(f"账号{idx}:\n  账号名: {name}\n  服务商: {issuer}\n  密钥: {secret}\n")
                    self._update_ui("\n".join(result_lines), error=False)
                except Exception as e:
                    self._update_ui(f"Error: Failed to parse migration QR code: {str(e)}", error=True)
                return
            else:
                self._update_ui(
                    "Error: Invalid QR code format. Not a Google Authenticator QR code.",
                    error=True
                )
                return
        except Exception as e:
            self._update_ui(f"Error: An error occurred: {str(e)}", error=True)

    def _update_ui(self, message, error=False):
        def update():
            self.result_text.delete(1.0, tk.END)
            self.result_text.insert(tk.END, message)
            self.status_label.config(
                text="Error" if error else "完成",
                foreground="red" if error else "black"
            )
        self.root.after(0, update)

    def show_about(self):
        about_text = (
            "2FA 二维码识别工具\n\n"
            "【使用说明】\n"
            "本程序用于识别和解析 Google Authenticator 二维码,包括普通 otpauth:// 以及迁移 otpauth-migration:// 类型。\n"
            "选择二维码图片后,程序会自动解析出 2FA 密钥或迁移信息。\n\n"
            "【声明与授权要求】\n"
            "本软件仅供学习、交流与个人正当用途。\n"
            "作者不对因使用本软件造成的任何数据丢失、隐私泄露或其他后果负责。\n"
            "严禁将本软件用于任何非法用途,否则后果自负。\n\n"
            "【作者】\n"
            "🍠小红书:奇技淫巧的安迪\n"
            "🌐网站:hi-andy.com\n"
        )
        messagebox.showinfo("关于", about_text)

def main():
    root = tk.Tk()
    # 用 base64 内嵌方式设置窗口图标
    ico_data = b'''XXXX'''
    with tempfile.NamedTemporaryFile(delete=False, suffix='.ico') as tmp_ico:
        tmp_ico.write(base64.b64decode(ico_data))
        tmp_ico_path = tmp_ico.name
    root.iconbitmap(tmp_ico_path)
    app = QRCodeScanner(root)
    root.mainloop()

if __name__ == '__main__':
    main()