Naposledy aktivní 1 month ago

将 TOTP 二维码转换为 Markdown 表格

Revize 5dd51a045c20ec3b1f04663f19a117ea0de24098

totp2md.py Raw
1#!/usr/bin/env python3
2
3#============================================================
4# File: totp2md.py
5# Description: 将 TOTP 二维码转换为 Markdown 表格
6# URL: https://s.fx4.cn/
7# ORIGIN: https://gist.asfd.cn/jetsung/totp2md/raw/HEAD/totp2md.py
8# Author: Jetsung Chan <[email protected]>
9# Version: 0.1.0
10# CreatedAt: 2025-08-18
11# UpdatedAt: 2025-08-18
12#============================================================
13
14import os
15import re
16import subprocess
17import urllib.parse
18from pathlib import Path
19import argparse
20
21
22def decode_qr_image(file_path):
23 """使用 zbarimg 读取二维码内容"""
24 try:
25 result = subprocess.run(
26 ['zbarimg', str(file_path)],
27 stdout=subprocess.PIPE,
28 stderr=subprocess.PIPE,
29 text=True,
30 check=True
31 )
32 lines = result.stdout.strip().splitlines()
33 if lines:
34 qr_text = lines[0].strip()
35 if qr_text.startswith("QR-Code:"):
36 qr_text = qr_text[len("QR-Code:"):]
37 return qr_text.strip()
38 except subprocess.CalledProcessError:
39 pass # zbarimg 无法识别
40 except FileNotFoundError:
41 print("错误: 找不到 zbarimg,请确保已安装 ZBar 并加入 PATH。")
42 exit(1)
43 return None
44
45
46def parse_otpauth_uri(uri):
47 """解析 otpauth://totp/ URI"""
48 sitename = ""
49 username = ""
50 secret = ""
51 issuer = ""
52
53 try:
54 from urllib.parse import urlparse, parse_qs
55 parsed = urlparse(uri)
56 query = parse_qs(parsed.query, keep_blank_values=True)
57 secret = query.get('secret', [None])[0] or ""
58
59 label = parsed.path.lstrip('/') # e.g., Google:[email protected]
60 label = urllib.parse.unquote(label)
61
62 issuer = query.get('issuer', [None])[0] or ""
63 if issuer:
64 issuer = urllib.parse.unquote(issuer)
65
66 if ':' in label:
67 parts = label.split(':', 1)
68 sitename = parts[0].strip()
69 username = parts[1].strip()
70 else:
71 username = label
72
73 if not issuer:
74 issuer = sitename or ""
75
76 except Exception as e:
77 print(f"解析 otpauth 失败: {e}")
78 return sitename, username, secret, issuer
79
80
81def parse_ms_msa_uri(uri):
82 """解析 ms-msa:// URI,提取 uaid 和 code"""
83 username = ""
84 secret = ""
85 try:
86 query_string = uri.split('//', 1)[1]
87 # 使用 parse_qs 解析
88 query = urllib.parse.parse_qs(query_string, keep_blank_values=True)
89
90 username = query.get('uaid', [None])[0] or ""
91 secret = query.get('code', [None])[0] or ""
92
93 # 返回值:sitename, username, secret, issuer
94 return "", username, secret, "Microsoft"
95 except Exception as e:
96 print(f"解析 ms-msa 失败: {e}")
97 return "", "", "", "Microsoft"
98
99
100def main(input_dir, output_file):
101 file_dir = Path(input_dir)
102 if not file_dir.exists():
103 print(f"目录不存在: {file_dir}")
104 exit(1)
105
106 # 获取所有 .svg 和 .png 文件
107 image_files = []
108 image_files.extend(file_dir.rglob("*.svg"))
109 image_files.extend(file_dir.rglob("*.png"))
110
111 # 按路径排序
112 image_files.sort(key=lambda x: str(x).lower())
113
114 # 写入 Markdown 表头
115 with open(output_file, 'w', encoding='utf-8') as f:
116 f.write("| SiteName | Username | Secret | Text | Mark |\n")
117 f.write("|:---|:---|:---|:---|:---|\n")
118
119 for file_path in image_files:
120 qr_text = decode_qr_image(file_path)
121 if not qr_text:
122 print(f"无法读取二维码: {file_path}")
123 continue
124
125 # 转义 Markdown 中的 |
126 text_display = qr_text.replace('|', r'\|')
127
128 sitename = ""
129 username = ""
130 secret = ""
131 issuer = ""
132
133 if qr_text.startswith("otpauth://totp/"):
134 sitename, username, secret, issuer = parse_otpauth_uri(qr_text)
135 if not issuer:
136 issuer = sitename or file_path.stem
137 elif qr_text.startswith("ms-msa://"):
138 sitename, username, secret, issuer = parse_ms_msa_uri(qr_text)
139 else:
140 # 其他类型
141 sitename = file_path.stem
142 username = ""
143 secret = ""
144 issuer = file_path.stem
145
146 # 构造 SiteName 列:[issuer](相对路径)
147 relative_path = file_path.as_posix()
148 issuer_str = f"[{issuer}]({relative_path})"
149
150 # 输出调试信息
151 print(f"filename: {file_path}")
152 print(f"sitename: {issuer}")
153 print(f"username: {username}")
154 print(f"secret: {secret}")
155 print(f"text: {qr_text}")
156 print()
157
158 # 写入表格行
159 f.write(f"| {issuer_str} | {username} | {secret} | `{text_display}` | |\n")
160
161 print(f"✅ Markdown 表格已生成: {output_file}")
162
163
164if __name__ == "__main__":
165 parser = argparse.ArgumentParser(description="从 TOTP 二维码图片生成 Markdown 表格")
166 parser.add_argument(
167 '-i', '--input',
168 default="TOTP",
169 help="二维码图片目录 (默认: TOTP)"
170 )
171 parser.add_argument(
172 '-o', '--output',
173 default="qr_codes.md",
174 help="输出 Markdown 文件路径 (默认: qr_codes.md)"
175 )
176
177 args = parser.parse_args()
178 main(args.input, args.output)
179
180###
181#
182# 依赖 zbarimg
183# Windows:https://zbar.sourceforge.net/
184# Linux:https://github.com/mchehab/zbar
185#
186###
187
totp2md.sh Raw
1#!/usr/bin/env bash
2
3#============================================================
4# File: totp2md.sh
5# Description: 将 TOTP 二维码转换为 Markdown 表格
6# URL: https://s.fx4.cn/
7# ORIGIN: https://gist.asfd.cn/jetsung/totp2md/raw/HEAD/totp2md.sh
8# Author: Jetsung Chan <[email protected]>
9# Version: 0.1.0
10# CreatedAt: 2025-08-18
11# UpdatedAt: 2025-08-18
12#============================================================
13
14
15if [[ -n "${DEBUG:-}" ]]; then
16 set -eux
17else
18 set -euo pipefail
19fi
20
21TOTP_DIR="${1:-TOTP}"
22OUTPUT_FILE="${2:-qr_codes.md}"
23
24# Initialize Markdown table header
25echo "| SiteName | Username | Secret | Text | Mark |" > "$OUTPUT_FILE"
26echo "|:---|:---|:---|:---|:---|" >> "$OUTPUT_FILE"
27
28# Find all SVG and PNG files in the specified directory
29find "$TOTP_DIR" -type f \( -name "*.svg" -o -name "*.png" \) | while read -r file; do
30 qr_text=$(zbarimg "$file" | head -n 1 2>/dev/null)
31 qr_text=${qr_text/QR-Code:/}
32 # qr_text=$(qrtool decode "$file" 2>/dev/null)
33 # qr_text=$(totp-qr --uri "$file" 2>/dev/null)
34
35 filename="$file"
36
37 sitename=""
38 username=""
39 secret=""
40 text="$qr_text"
41 issuer=""
42
43 # # Handle otpauth URLs
44 if [[ "$qr_text" =~ ^otpauth://totp/ ]]; then
45 label=$(echo "$qr_text" | grep -oP '(?<=totp/).*?(?=\?)')
46 label=$(printf '%b' "${label//%/\\x}")
47
48 issuer=$(echo "$qr_text" | grep -oP 'issuer=\K[^&]*' | head -1)
49 issuer=$(printf '%b' "${issuer//%/\\x}")
50
51 if [ -n "$label" ]; then
52 if [[ "$label" =~ ^([^:]+):(.+)$ ]]; then
53 sitename="${BASH_REMATCH[1]}"
54 username="${BASH_REMATCH[2]}"
55 else
56 username="$label"
57 fi
58 fi
59
60 if [ -z "$issuer" ]; then
61 issuer="$sitename"
62 fi
63
64 secret=$(echo "$qr_text" | grep -oP 'secret=\K[^&]*' | head -1)
65
66 elif [[ "$qr_text" =~ ^ms-msa:// ]]; then
67 username=$(echo "$qr_text" | grep -oP 'uaid=\K[^&]*' | head -1)
68 secret=$(echo "$qr_text" | grep -oP 'code=\K[^&]*' | head -1)
69 else
70 sitename="$filename"
71 username=""
72 secret=""
73 fi
74
75 text=${text//|/\\|}
76
77 if [ -z "$issuer" ]; then
78 issuer=$(basename "$filename")
79 fi
80 issuer_str="[$issuer]($filename)"
81
82 echo "filename: $filename"
83 echo "sitename: $issuer"
84 echo "username: $username"
85 echo "secret: $secret"
86 echo "text: $text"
87 echo
88
89 # shellcheck disable=SC2006
90 echo "| $issuer_str | $username | $secret | `$text` | |" >> "$OUTPUT_FILE"
91done
92
93echo "Markdown table generated in $OUTPUT_FILE"
94
95###
96#
97# 参数1: TOTP 目录
98# 参数2: 输出文件
99#
100# 示例:
101# ./totp2md.sh TOTP qr_codes.md
102#
103###
104