--

背景

这是一个完全由 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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
<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="Base32 key or otpauth:// URI" autocomplete="off" autocapitalize="characters" spellcheck="false" 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="0"></progress></div>

<p class="totp-error" role="alert" aria-live="polite"></p>

<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 showTOTPError(message) {
clearTimeout(countdown);
clearInterval(countdownInterval);
document.getElementById('token').innerText = '--';
document.querySelector('#totp .progress').value = 0;
document.querySelector('#totp .is-size-7').innerText = '';
document.querySelector('#totp .totp-error').innerText = message;
}

function parseSecret(value) {
var secret = value.trim();
var options = {};

if (/^otpauth:\/\//i.test(secret)) {
var uri = new URL(secret);
secret = uri.searchParams.get('secret') || '';
options.algorithm = uri.searchParams.get('algorithm') || '';
options.digits = uri.searchParams.get('digits') || '';
options.period = uri.searchParams.get('period') || '';
}

secret = secret.replace(/[\s-]/g, '').toUpperCase();
if (!secret || !/^[A-Z2-7]+=*$/.test(secret)) {
throw new Error('The secret must be Base32 text or an otpauth URI.');
}

return { secret: secret, options: options };
}

function positiveInteger(value, fallback) {
var parsed = parseInt(value, 10);
return parsed > 0 ? parsed : fallback;
}

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

var secretInput = document.getElementById('secret');
var periodInput = document.getElementById('period');
var digitsInput = document.getElementById('digits');

if (!secretInput.value.trim()) {
showTOTPError('Enter a secret key to generate a code.');
return;
}

try {
var parsedSecret = parseSecret(secretInput.value);
var period = positiveInteger(parsedSecret.options.period, positiveInteger(periodInput.value, 30));
var digits = positiveInteger(parsedSecret.options.digits, positiveInteger(digitsInput.value, 6));
var algorithm = (parsedSecret.options.algorithm || 'SHA1').toUpperCase();

if (!/^(SHA1|SHA256|SHA512)$/.test(algorithm)) {
throw new Error('The algorithm must be SHA1, SHA256, or SHA512.');
}

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

document.getElementById('token').innerText = totp.generate();
document.querySelector('#totp .totp-error').innerText = '';

var updateCountdown = function() {
var remainingTime = period - (Math.floor(Date.now() / 1000) % period);
updateProgressBarAndSpan(period, remainingTime);
return remainingTime;
};
var remainingTime = updateCountdown();

countdownInterval = setInterval(updateCountdown, 1000);
countdown = setTimeout(generateTOTP, remainingTime * 1000);
} catch (error) {
showTOTPError(error.message || 'Unable to generate a TOTP code.');
}
}

generateTOTP();

</script>

Background

This is a small tool entirely written by AI, used to generate TOTP keys.

Let me explain in detail what it does.

In short, it solves the following problem:

Two or three years ago, I installed Microsoft Authenticator on my phone, and ever since, every login to my Microsoft account has demanded authentication via Authenticator. I kept thinking — what if I lose my phone? Is there any other way to recover my account?

After searching the internet, the general conclusion was “extremely difficult”.

So I started maintaining that app very carefully. When I switched phones in between, I migrated the Authenticator data over so nothing would be lost.

But that still didn’t solve the core problem: what if I lose my phone?

Later, I found this project: https://github.com/Bubka/2FAuth

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

This project can generate two-factor verification codes, and the key point is it’s a “web app” — it no longer depends on my phone. As long as I run it on a computer or server, I can get my codes anytime, anywhere without worrying about losing my phone.
(Of course, you still need to back up such important data — computers and servers can break too.)

While researching how to use this project, I discovered another professional password manager that can also generate two-factor codes: Bitwarden.

It has an unofficial open-source implementation: https://github.com/dani-garcia/vaultwarden

So the further question is: how do they generate these codes?

The answer is TOTP.

TOTP stands for Time-based One-Time Password. It’s an algorithm used for authentication, typically for multi-factor authentication (MFA).

That is, it generates a one-time password from an input secret (i.e. a string) and the current timestamp. Microsoft, Oracle, Google… virtually every foreign company uses this algorithm.

Whether it’s Oracle Cloud’s authenticator, GitHub’s authenticator, Microsoft’s authenticator, or Google’s authenticator, they’re all just apps built on this algorithm.

When these foreign sites ask you to enable two-factor verification, you need to click some non-recommended options like: I can’t download apps, I can’t scan QR codes, I choose to enter the secret manually, etc.

Then you’ll get a TOTP secret, a string like: JBSWY3DPEHPK3PXP

Enter it here and you’ll get a code.

  1. Keep your secret safe — it’s the only credential for generating codes.

  2. Note that it’s a time-based algorithm; make sure your device’s clock is accurate.

  3. The 30s refresh period and 6-digit codes are the defaults; in most cases you won’t need to change them.

Code

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
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
<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; /* Green */
}

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

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

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

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

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

#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="Base32 key or otpauth:// URI" autocomplete="off" autocapitalize="characters" spellcheck="false" 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="0"></progress></div>

<p class="totp-error" role="alert" aria-live="polite"></p>

<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');


// Reset the progress bar and text colors
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 showTOTPError(message) {
clearTimeout(countdown);
clearInterval(countdownInterval);
document.getElementById('token').innerText = '--';
document.querySelector('#totp .progress').value = 0;
document.querySelector('#totp .is-size-7').innerText = '';
document.querySelector('#totp .totp-error').innerText = message;
}

function parseSecret(value) {
var secret = value.trim();
var options = {};

if (/^otpauth:\/\//i.test(secret)) {
var uri = new URL(secret);
secret = uri.searchParams.get('secret') || '';
options.algorithm = uri.searchParams.get('algorithm') || '';
options.digits = uri.searchParams.get('digits') || '';
options.period = uri.searchParams.get('period') || '';
}

secret = secret.replace(/[\s-]/g, '').toUpperCase();
if (!secret || !/^[A-Z2-7]+=*$/.test(secret)) {
throw new Error('The secret must be Base32 text or an otpauth URI.');
}

return { secret: secret, options: options };
}

function positiveInteger(value, fallback) {
var parsed = parseInt(value, 10);
return parsed > 0 ? parsed : fallback;
}

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

var secretInput = document.getElementById('secret');
var periodInput = document.getElementById('period');
var digitsInput = document.getElementById('digits');

if (!secretInput.value.trim()) {
showTOTPError('Enter a secret key to generate a code.');
return;
}

try {
var parsedSecret = parseSecret(secretInput.value);
var period = positiveInteger(parsedSecret.options.period, positiveInteger(periodInput.value, 30));
var digits = positiveInteger(parsedSecret.options.digits, positiveInteger(digitsInput.value, 6));
var algorithm = (parsedSecret.options.algorithm || 'SHA1').toUpperCase();

if (!/^(SHA1|SHA256|SHA512)$/.test(algorithm)) {
throw new Error('The algorithm must be SHA1, SHA256, or SHA512.');
}

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

document.getElementById('token').innerText = totp.generate();
document.querySelector('#totp .totp-error').innerText = '';

var updateCountdown = function() {
var remainingTime = period - (Math.floor(Date.now() / 1000) % period);
updateProgressBarAndSpan(period, remainingTime);
return remainingTime;
};
var remainingTime = updateCountdown();

countdownInterval = setInterval(updateCountdown, 1000);
countdown = setTimeout(generateTOTP, remainingTime * 1000);
} catch (error) {
showTOTPError(error.message || 'Unable to generate a TOTP code.');
}
}

generateTOTP();

</script>