mirror of
https://github.com/wuzf/2fa.git
synced 2026-09-03 07:17:09 +08:00
update
This commit is contained in:
13
.editorconfig
Normal file
13
.editorconfig
Normal 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
172
.gitignore
vendored
Normal 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
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"printWidth": 140,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"useTabs": true
|
||||
}
|
||||
58
README.md
58
README.md
@@ -1,10 +1,54 @@
|
||||
# 2fa
|
||||
这是一个用Cloudflare Worker代码实现的Two Factor Authentication,worker页面会显示动态生成的 One-Time Password(OTP),并且能够在 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
1264
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
package.json
Normal file
16
package.json
Normal 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
136
src/worker.js
Normal 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;
|
||||
}
|
||||
90
worker.js
90
worker.js
@@ -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
4
wrangler.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
name = "2fa"
|
||||
main = "src/worker.js"
|
||||
compatibility_date = "2024-01-13"
|
||||
workers_dev = true
|
||||
Reference in New Issue
Block a user