注意:这篇文章上次更新于167天前,文章内容可能已经过时。
背景
这是一个完全由 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
把它输入到这里,你就获得了一个验证码。
你需要保存好你的密钥,因为这个密钥是生成验证码的唯一凭证。
注意它是 Time-based 的算法,使用它请确保你的设备时间准确。
30s 的刷新周期和 6 位的验证码是默认的设置,绝大多数情况不需要修改。
代码
<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 */
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 */
.content {
margin-top: 20px;
}
.has-text-grey {
display: block;
color: #666;
}
/* Box for the TOTP token */
.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 */
#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 */
}
.title {
font-size: 2rem;
margin: 0;
word-break: break-all;
}
/* Base styles for the progress bar */
.progress {
width: 100%;
height: 15px;
border-radius: 5px;
overflow: hidden;
}
.progress.is-info::-webkit-progress-value {
background-color: #00d1b2; /* 绿色 */
}
.progress.is-warning::-webkit-progress-value {
background-color: #ffc107; /* 黄色 */
}
.progress.is-danger::-webkit-progress-value {
background-color: #ff3860; /* 红色 */
}
.progress.is-info::-moz-progress-bar {
background-color: #00d1b2; /* 绿色 */
}
.progress.is-warning::-moz-progress-bar {
background-color: #ffc107; /* 黄色 */
}
.progress.is-danger::-moz-progress-bar {
background-color: #ff3860; /* 红色 */
}
.has-text-grey {
color: #4a4a4a; /* Original color */
}
.has-text-warning {
color: #ffc107; /* Yellow color */
}
.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>