This commit is contained in:
wuzf
2024-01-20 01:22:59 +08:00
parent 5c87373418
commit 59b8648fa9
9 changed files with 1662 additions and 97 deletions

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
# http://editorconfig.org
root = true
[*]
indent_style = tab
tab_width = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_style = space

172
.gitignore vendored Normal file
View File

@@ -0,0 +1,172 @@
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*
# wrangler project
.dev.vars
.wrangler/

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"printWidth": 140,
"singleQuote": true,
"semi": true,
"useTabs": true
}

View File

@@ -1,10 +1,54 @@
# 2fa
这是一个用Cloudflare Worker代码实现的Two Factor Authenticationworker页面会显示动态生成的 One-Time PasswordOTP并且能够在 OTP 过期后自动刷新以提供新的 OTP可以简单代替Google Authenticator或者Microsoft Authenticator。
如果不想用演示站也可以自己复制worker.js源码通过Cloudflare Worker自行搭建
# 2FA OTP Generator
这是一个用Cloudflare Worker实现的Two Factor Authentication (2FA) OTP生成器可以简单代替Google Authenticator或Microsoft Authenticator
## 功能特点
- 🔐 基于TOTP (Time-based One-Time Password) 算法
- ⏰ 30秒自动刷新OTP
- 📱 支持所有支持2FA的服务GitHub、Google、Microsoft等
- 🔄 自动刷新,无需手动操作
## 部署方法
直接复制worker.js源码到Cloudflare Workers部署。
## 使用方法
### 1. 访问OTP生成器
在URL后添加您的2FA密钥
```
https://your-domain.com/YOUR_SECRET_KEY
```
**示例:**
```
https://your-domain.com/JBSWY3DPEHPK3PXP
```
### 2. 获取OTP
页面会显示JSON格式的OTP
```json
{
"token": "123456"
}
```
OTP每30秒自动刷新无需手动刷新页面。
## 演示网站
### 演示网站
https://2fa.guts.eu.org/MBWJJVDIQ3PH3SA5
### 备用网站:
1. https://2fa.my2fa.workers.dev/MBWJJVDIQ3PH3SA5
2. https://tfa.mytfa.workers.dev/MBWJJVDIQ3PH3SA5
## 技术实现
- **算法**: TOTP (RFC 6238)
- **哈希算法**: HMAC-SHA1
- **时间步长**: 30秒
- **OTP长度**: 6位数字
## 许可证
MIT License

1264
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

16
package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "2fa",
"version": "0.0.0",
"private": true,
"scripts": {
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"start": "wrangler dev"
},
"devDependencies": {
"wrangler": "^3.0.0"
},
"dependencies": {
"otplib": "^12.0.1"
}
}

136
src/worker.js Normal file
View File

@@ -0,0 +1,136 @@
// 添加事件监听器
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
// 处理请求的主要函数
async function handleRequest(request) {
// 使用 new URL(request.url) 创建 URL 对象
const url = new URL(request.url);
// 从路径中提取密钥
const secret = url.pathname.substring(1);
// 检查密钥是否存在
if (!secret) {
const currentOrigin = url.origin; // 包含协议、域名和端口
return new Response(`Missing secret parameter!\n\nUsage: ${currentOrigin}/YOUR_SECRET_KEY\nExample: ${currentOrigin}/JBSWY3DPEHPK3PXP`, { status: 400 });
}
// 记录页面加载时的时间
const loadTime = Math.floor(Date.now() / 1000);
// 生成 OTP
const otp = await generateOTP(secret, loadTime);
// 构建 HTML 页面
const htmlContent = `
<html>
<head>
<title>OTP Page</title>
<script>
// 页面加载时的时间
const loadTime = ${loadTime};
// 计算 OTP 剩余有效时间
const remainingTime = ${calculateRemainingTime(loadTime)};
// 如果 OTP 剩余时间小于等于 0刷新页面
if (remainingTime <= 0) {
location.reload();
} else {
// 否则,延时刷新页面
setTimeout(() => {
location.reload();
}, remainingTime * 1000); // 将剩余时间转换为毫秒
}
</script>
</head>
<body>
<pre>{
"token": "${otp}"
}</pre>
</body>
</html>
`;
// 构建 HTML 格式的 Response
const htmlResponse = new Response(htmlContent, {
headers: {
'Content-Type': 'text/html'
}
});
return htmlResponse;
}
// 生成 OTP 的函数
async function generateOTP(secret, loadTime) {
// 获取当前时间戳的时间戳
const epochTime = Math.floor(Date.now() / 1000);
// 时间步长,这里假设是 30 秒
const timeStep = 30;
// 计算当前时间片
let counter = Math.floor(epochTime / timeStep);
// 计算当前时间片开始的时间
const counterStart = counter * timeStep;
// 将时间片转换为字节数组
const counterBytes = new Uint8Array(8);
for (let i = 7; i >= 0; i--) {
counterBytes[i] = counter & 0xff;
counter >>>= 8;
}
// 使用 crypto.subtle 计算 HMAC-SHA1
const key = await crypto.subtle.importKey(
'raw',
base32toByteArray(secret),
{ name: 'HMAC', hash: { name: 'SHA-1' } },
false,
['sign']
);
const hmacBuffer = await crypto.subtle.sign('HMAC', key, counterBytes.buffer);
const hmacArray = Array.from(new Uint8Array(hmacBuffer));
// 将结果转换为 OTP
const offset = hmacArray[hmacArray.length - 1] & 0xf;
const truncatedHash = hmacArray.slice(offset, offset + 4);
const otpValue = new DataView(new Uint8Array(truncatedHash).buffer).getUint32(0) & 0x7fffffff;
const otp = (otpValue % 1000000).toString().padStart(6, '0');
return otp;
}
// 辅助函数:将 Base32 编码的密钥转换为字节数组
function base32toByteArray(base32) {
const charTable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const base32Chars = base32.toUpperCase().split('');
const bits = base32Chars.map(char => charTable.indexOf(char).toString(2).padStart(5, '0')).join('');
const bytes = [];
for (let i = 0; i < bits.length; i += 8) {
bytes.push(parseInt(bits.slice(i, i + 8), 2));
}
return new Uint8Array(bytes);
}
// 辅助函数:计算 OTP 剩余有效时间
function calculateRemainingTime(loadTime) {
const epochTime = Math.floor(Date.now() / 1000);
const timeStep = 30; // 与生成 OTP 的时间步长相同
const currentCounter = Math.floor(epochTime / timeStep);
// 这里假设过期时间为 30 秒,您可以根据实际情况调整
const expirationTime = (currentCounter + 1) * timeStep;
// 计算剩余时间
const remainingTime = expirationTime - loadTime;
return remainingTime;
}

View File

@@ -1,90 +0,0 @@
(() => {
// src/index.js
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const url = new URL(request.url);
const secret = url.pathname.substring(1);
if (!secret) {
return new Response("Missing secret parameter", { status: 400 });
}
const loadTime = Math.floor(Date.now() / 1e3);
const otp = await generateOTP(secret, loadTime);
const htmlContent = `
<html>
<head>
<title>OTP Page</title>
<script>
const loadTime = ${loadTime};
const remainingTime = ${calculateRemainingTime(loadTime)};
if (remainingTime <= 0) {
location.reload();
} else {
setTimeout(() => {
location.reload();
}, remainingTime * 1000);
}
<\/script>
</head>
<body>
<pre>{
"token": "${otp}"
}</pre>
</body>
</html>
`;
const htmlResponse = new Response(htmlContent, {
headers: {
"Content-Type": "text/html"
}
});
return htmlResponse;
}
async function generateOTP(secret, loadTime) {
const epochTime = Math.floor(Date.now() / 1e3);
const timeStep = 30;
let counter = Math.floor(epochTime / timeStep);
const counterStart = counter * timeStep;
const counterBytes = new Uint8Array(8);
for (let i = 7; i >= 0; i--) {
counterBytes[i] = counter & 255;
counter >>>= 8;
}
const key = await crypto.subtle.importKey(
"raw",
base32toByteArray(secret),
{ name: "HMAC", hash: { name: "SHA-1" } },
false,
["sign"]
);
const hmacBuffer = await crypto.subtle.sign("HMAC", key, counterBytes.buffer);
const hmacArray = Array.from(new Uint8Array(hmacBuffer));
const offset = hmacArray[hmacArray.length - 1] & 15;
const truncatedHash = hmacArray.slice(offset, offset + 4);
const otpValue = new DataView(new Uint8Array(truncatedHash).buffer).getUint32(0) & 2147483647;
const otp = (otpValue % 1e6).toString().padStart(6, "0");
return otp;
}
function base32toByteArray(base32) {
const charTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
const base32Chars = base32.toUpperCase().split("");
const bits = base32Chars.map((char) => charTable.indexOf(char).toString(2).padStart(5, "0")).join("");
const bytes = [];
for (let i = 0; i < bits.length; i += 8) {
bytes.push(parseInt(bits.slice(i, i + 8), 2));
}
return new Uint8Array(bytes);
}
function calculateRemainingTime(loadTime) {
const epochTime = Math.floor(Date.now() / 1e3);
const timeStep = 30;
const currentCounter = Math.floor(epochTime / timeStep);
const expirationTime = (currentCounter + 1) * timeStep;
const remainingTime = expirationTime - loadTime;
return remainingTime;
}
})();
//# sourceMappingURL=index.js.map

4
wrangler.toml Normal file
View File

@@ -0,0 +1,4 @@
name = "2fa"
main = "src/worker.js"
compatibility_date = "2024-01-13"
workers_dev = true