// ==UserScript== // @name 本地2FA验证器 // @namespace http://tampermonkey.net/ // @version 12.1 // @description 一个纯本地、离线的2FA(TOTP)验证码生成器 // @author Gemini // @match *://*/* // @run-at document-idle // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant GM_addStyle // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/542673/%E6%9C%AC%E5%9C%B02FA%E9%AA%8C%E8%AF%81%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/542673/%E6%9C%AC%E5%9C%B02FA%E9%AA%8C%E8%AF%81%E5%99%A8.meta.js // ==/UserScript== (function() { 'use strict'; /* * ================================================================================= * INLINED LIBRARY: otpauth (Clean, unminified, correct source code) * ================================================================================= */ const otpauth = (() => { class OTPAuthError extends Error { constructor(message) { super(message); this.name = this.constructor.name; } } class Secret { constructor({ buffer } = {}) { if (!(buffer instanceof ArrayBuffer)) throw new OTPAuthError("Buffer must be an instance of 'ArrayBuffer'"); this._buffer = buffer; } get buffer() { return this._buffer; } static fromBase32(base32) { const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; const clean_base32 = base32.toUpperCase().replace(/=+$/, ''); const bitsPerChar = 5; const bytes = new Uint8Array(Math.floor(clean_base32.length * bitsPerChar / 8)); let bits = 0; let value = 0; let index = 0; for (let i = 0; i < clean_base32.length; i++) { const charIndex = alphabet.indexOf(clean_base32[i]); if (charIndex === -1) throw new OTPAuthError("Invalid Base32 character"); value = (value << bitsPerChar) | charIndex; bits += bitsPerChar; if (bits >= 8) { bytes[index++] = (value >>> (bits - 8)) & 255; bits -= 8; } } return new Secret({ buffer: bytes.buffer }); } } class TOTP { constructor({ secret, algorithm = 'SHA1', digits = 6, period = 30 } = {}) { if (!(secret instanceof Secret)) throw new OTPAuthError("Secret must be an instance of 'Secret'"); this.secret = secret; this.algorithm = algorithm; this.digits = digits; this.period = period; } async generate({ timestamp = Date.now() } = {}) { const counter = Math.floor(timestamp / 1000 / this.period); const counterBuffer = new ArrayBuffer(8); const counterView = new DataView(counterBuffer); counterView.setUint32(0, Math.floor(counter / 4294967296)); counterView.setUint32(4, counter & 0xFFFFFFFF); const cryptoAlgo = { name: 'HMAC', hash: `SHA-${this.algorithm.slice(3)}` }; const key = await crypto.subtle.importKey('raw', this.secret.buffer, cryptoAlgo, false, ['sign']); const signature = await crypto.subtle.sign('HMAC', key, counterBuffer); const signatureView = new DataView(signature); const offset = signatureView.getUint8(signatureView.byteLength - 1) & 0x0f; let value = signatureView.getUint32(offset); value &= 0x7fffffff; value %= Math.pow(10, this.digits); return value.toString().padStart(this.digits, '0'); } } return { Secret, TOTP }; })(); /* ================================================================================= * STYLING (CENTERED & SHARP EDGES) * ================================================================================= */ GM_addStyle(` #totp-container { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 320px; background-color: #f9f9f9; border: 1px solid #ccc; border-radius: 0; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 999999; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; color: #333; } #totp-header { padding: 5px 15px; cursor: move; background-color: #efefef; border-bottom: 1px solid #ccc; display: flex; justify-content: space-between; align-items: center; } #totp-header h3 { margin: 0; font-size: 14px; font-weight: 600; color: #333333; } #totp-close-btn { cursor: pointer; font-size: 20px; font-weight: bold; color: #888; border: none; background: none; } #totp-search-container { padding: 1px; border-bottom: 1px solid #ccc; } #totp-search-box { width: 100%; box-sizing: border-box; padding: 4px; border: 1px solid #ccc; border-radius: 0; font-size: 4px; background-color: #F9F9F9; color: #333333; height: 26px; } #totp-list { list-style: none; padding: 10px; margin: 0; max-height: 400px; overflow-y: auto; transition: height 0.2s; scrollbar-color: #8B8B8B #F9F9F9; } .totp-item { display: flex; flex-direction: column; padding: 12px; border-bottom: 1px solid #CCCCCC; } .totp-item:last-child { border-bottom: none; } .totp-item-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } .totp-name { font-size: 14px; font-weight: 500; color: #333333; } .totp-delete-btn { cursor: pointer; color: #f44336; font-size: 12px; border: 1px solid #f44336; border-radius: 0; padding: 2px 6px; background-color: white; } .totp-delete-btn:hover { background-color: #f44336; color: white; } .totp-code { font-size: 20px; font-weight: bold; letter-spacing: 2px; color: #007bff; cursor: pointer; text-align: center; min-height: 20px; display: flex; align-items: center; justify-content: center; } .totp-progress-bar { width: 100%; height: 4px; background-color: #e9ecef; border-radius: 0; margin-top: 8px; overflow: hidden; } .totp-progress { height: 100%; background-color: #007bff; transition: width 1s linear; } #totp-add-btn-container { padding: 10px; border-top: 1px solid #ccc; text-align: center; } #totp-add-btn { width: 100%; padding: 8px; font-size: 14px; cursor: pointer; background-color: #28a745; color: white; border: none; border-radius: 0; } `); /* ================================================================================= * UI & CORE LOGIC * ================================================================================= */ const container = document.createElement('div'); container.id = 'totp-container'; container.innerHTML = `
暂无密钥,请点击下方按钮添加。
'; return; } const sortedSecrets = new Map([...secretsMap.entries()].sort()); for (const [name, secret] of sortedSecrets.entries()) { const item = document.createElement('div'); item.className = 'totp-item'; item.setAttribute('data-name', name); item.innerHTML = `