背景

这是一个完全由 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 位的验证码是默认的设置,绝大多数情况不需要修改。

代码

<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>