背景

这是一个完全由 AI 编写的小工具,用于生成 TOTP 密钥。

让我来详细解释一下它是用来干什么的。

简单来说,就是为了解决下面这个问题的:

两三年前,手机下载了微软的 Authenticator,然后我的微软账号每次登录就一直要求我用 Authenticator 来认证。我当时就在想,如果我手机丢了怎么办,还有什么办法找回我的账号吗?

网上搜索了一圈,得到的结论基本就是“极其困难”。

于是,我便开始小心翼翼的维护这个软件,中间换了一次手机,我也是把 Authenticator 的数据迁移过去,以免丢失。

但是,这还是没有解决那个问题:如果我手机丢了呢?

后来,我发现了这个项目:https://github.com/Bubka/2FAuth

A web app to manage your Two-Factor Authentication (2FA) accounts and generate their security codes

这个项目可以用来生成二步验证的验证码,而且重点是 “web app”,也就是说,它不再依赖我的手机,只要我把它在电脑或者服务器上运行起来,我就可以随时随地的获取到我的验证码而不用担心手机丢失。
(当然,你需要对这样重要的数据进行备份,毕竟即使是电脑和服务器也是会坏的。)

在查询这个项目怎么应用的过程中,我发现了另一个专业的密码管理器也能用于生成二步验证的验证码,那就是 Bitwarden。

它有一个非官方的开源实现:https://github.com/dani-garcia/vaultwarden

那么更进一步的问题是,他们是怎么生成这些验证码的呢?

答案就是 TOTP。

TOTP 是 Time-based One-Time Password 的缩写,即基于时间的一次性密码。它是一种用于身份验证的算法,通常用于多因素身份验证(MFA)。

也就是说,它根据一个输入的密钥(也就是一个字符串)和当前的时间戳生成一个一次性的密码,微软,甲骨文,谷歌… 等等几乎所有国外的公司都在使用这种算法。

无论是甲骨文云的验证器、GitHub 的验证器、微软的验证器、谷歌的验证器,都是基于这个算法的一个 APP 而已。

当这些国外网站要求我们开启两步验证的时候,你需要点击一些非推荐性的选项,例如:我不能下载 APP,我不能扫描二维码,我选择手动输入密钥 等等

然后你就会获得一个 TOTP 密钥,像是这样的字符串:JBSWY3DPEHPK3PXP

把它输入到这里,你就获得了一个验证码。

  1. 你需要保存好你的密钥,因为这个密钥是生成验证码的唯一凭证。

  2. 注意它是 Time-based 的算法,使用它请确保你的设备时间准确。

  3. 30s 的刷新周期和 6 位的验证码是默认的设置,绝大多数情况不需要修改。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
<style>
/* Base styles for the TOTP container */
#totp {
font-family: Arial, sans-serif;
color: #333;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
margin: 20px auto;
padding: 20px;
box-sizing: border-box;
width: 100%;
max-width: 600px;
}

/* Style for the labels - now bold */
#totp label.totp {
display: block;
margin-top: 10px;
margin-bottom: 5px;
color: #555;
font-weight: bold; /* Make the label text bold */
}

#totp input[type="text"],
#totp input[type="number"] {
display: block;
width: 100%; /* Adjust width to account for padding and border */
padding: 10px;
margin-bottom: 10px; /* Add space below each input */
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
line-height: 1.5;
}

/* Progress bar and remaining time styles */
#totp .content {
margin-top: 20px;
}

#totp .has-text-grey {
display: block;
color: #666;
}

/* Box for the TOTP token */
#totp .box {
text-align: center;
margin-top: 20px;
border: solid 1px #dbe2e8;
background: #f7f9fc;
display: flex; /* Use flexbox to center the content */
justify-content: center; /* Center horizontally */
align-items: center; /* Center vertically */
padding: 0; /* Reset padding */
}

/* Style for the token text - ensures it is centered and bold */
#totp #token {
font-size: 3rem; /* Increased font size for the token */
font-weight: bold; /* Makes the token text bold */
margin: 0; /* Reset margin */
word-break: break-all;
flex: 0 0 auto; /* Do not grow or shrink */
}

#totp .title {
font-size: 2rem;
margin: 0;
word-break: break-all;
}


/* Base styles for the progress bar */
#totp .progress {
width: 100%;
height: 15px;
border-radius: 5px;
overflow: hidden;
}
#totp .progress.is-info::-webkit-progress-value {
background-color: #00d1b2; /* 绿色 */
}

#totp .progress.is-warning::-webkit-progress-value {
background-color: #ffc107; /* 黄色 */
}

#totp .progress.is-danger::-webkit-progress-value {
background-color: #ff3860; /* 红色 */
}

#totp .progress.is-info::-moz-progress-bar {
background-color: #00d1b2; /* 绿色 */
}

#totp .progress.is-warning::-moz-progress-bar {
background-color: #ffc107; /* 黄色 */
}

#totp .progress.is-danger::-moz-progress-bar {
background-color: #ff3860; /* 红色 */
}

#totp .has-text-grey {
color: #4a4a4a; /* Original color */
}

#totp .has-text-warning {
color: #ffc107; /* Yellow color */
}

#totp .has-text-danger {
color: #ff3860; /* Red color */
}

</style>


<div id="totp">
<label for="secret" class="totp">Secret Key:</label>
<input type="text" id="secret" placeholder="Enter your secret key" oninput="generateTOTP()">

<label for="period" class="totp">Period in seconds:</label>
<input type="number" id="period" placeholder="Enter period in seconds" value="30" oninput="generateTOTP()">

<label for="digits" class="totp">Number of digits:</label>
<input type="number" id="digits" placeholder="Enter number of digits" value="6" oninput="generateTOTP()">

<div class="content"><span class="has-text-grey is-size-7"></span><progress class="progress is-info" max="30" value="3"></progress></div>

<div class="box"><p class="title is-size-1 has-text-centered" id="token"></p></div>

</div>

<script src="https://unpkg.com/otpauth@9.2.4/dist/otpauth.umd.min.js"></script>

<script>
var countdown;
var countdownInterval;

function updateProgressBarAndSpan(maxValue, remaining) {
var progressBar = document.querySelector('.progress');

var remainingTimeSpan = document.querySelector('.is-size-7');


// 重置进度条和文本颜色
progressBar.classList.remove('is-info', 'is-warning', 'is-danger');
remainingTimeSpan.classList.remove('has-text-warning', 'has-text-danger');
// Change class based on remaining time
if (remaining < 5) {
progressBar.classList.add('is-danger');
remainingTimeSpan.classList.add('has-text-grey', 'has-text-danger');
} else if (remaining < 10) {
progressBar.classList.add('is-warning');
remainingTimeSpan.classList.add('has-text-grey', 'has-text-warning');
}
progressBar.max = maxValue;
progressBar.value = remaining;
remainingTimeSpan.innerText = 'Remaining time: ' + remaining + 's';
}


function generateTOTP() {
clearTimeout(countdown);
clearInterval(countdownInterval);

var secret = document.getElementById('secret').value;
var period = parseInt(document.getElementById('period').value) || 30;
var digits = parseInt(document.getElementById('digits').value) || 6;

var totp = new OTPAuth.TOTP({
secret: OTPAuth.Secret.fromBase32(secret),
digits: digits,
period: period,
algorithm: 'SHA1'
});

var totpCode = totp.generate();
document.getElementById('token').innerText = totpCode;

var remainingTime = period - Math.floor((Date.now() / 1000) % period);
updateProgressBarAndSpan(period, remainingTime);

countdownInterval = setInterval(function() {
remainingTime--;
if (remainingTime < 0) {
remainingTime = period;
}
updateProgressBarAndSpan(period, remainingTime);
}, 1000);

countdown = setTimeout(generateTOTP, remainingTime * 1000);
}

// Call generateTOTP once on page load to initialize
generateTOTP();

</script>