Last active
December 10, 2025 02:42
-
-
Save PrintNow/03829be994c2866de0ed7963b94e41d2 to your computer and use it in GitHub Desktop.
AWS 自动登录脚本 (包括自动填充 OTOP)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name AWS 自动登录脚本 | |
| // @namespace https://nowtime.cc | |
| // @version v1.4.0 | |
| // @description 自动登录 AWS 控制台, 2025-12-10 更新 | |
| // @author Shine | |
| // @match https://signin.aws.amazon.com/oauth?* | |
| // @match https://*.signin.aws.amazon.com/oauth?* | |
| // @icon https://www.google.com/s2/favicons?sz=64&domain=amazon.com | |
| // @grant GM.setValue | |
| // @grant GM.getValue | |
| // @grant GM.deleteValue | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (async function () { | |
| 'use strict'; | |
| const context = { | |
| otopConfig: {}, | |
| username: undefined, | |
| password: undefined, | |
| } | |
| // 初始化数据 | |
| window.addEventListener('load', await initData(() => { | |
| setTimeout(() => { | |
| console.log('开始执行自动登录 AWS') | |
| handleToLogin() | |
| }, 800) | |
| })); | |
| async function initData(then) { | |
| let otopURL = await GM.getValue('otopURL'), | |
| username = await GM.getValue('username'), | |
| password = await GM.getValue('password'); | |
| if (!otopURL) { | |
| otopURL = prompt("请输入 OTPAuth URL") | |
| if (otopURL === '') { | |
| throw new Error(`请输入 OTPAuth URL`) | |
| } | |
| GM.setValue('otopURL', otopURL); | |
| console.log('otopURL 设置成功') | |
| } | |
| if (!username) { | |
| username = prompt("请输入 AWS 用户名") | |
| if (username === '') { | |
| throw new Error(`请输入用户名`) | |
| } | |
| GM.setValue('username', username); | |
| console.log('AWS 用户名设置成功') | |
| } | |
| if (!password) { | |
| password = prompt("请输入 AWS 密码") | |
| if (password === '') { | |
| throw new Error(`请输入密码`) | |
| } | |
| GM.setValue('password', password); | |
| console.log('AWS 密码设置成功') | |
| } | |
| context.username = username; | |
| context.password = password; | |
| context.otopConfig = parseOtpauthURL(otopURL); | |
| then && then() | |
| } | |
| /** | |
| * | |
| * @param attempt 重试次数 | |
| * @param action 0:输入账号密码页面,1:输入 OTP 页面 | |
| */ | |
| async function handleToLogin(attempt = 0, action = 0) { | |
| // 如果实在输入账号密码页面 | |
| if (action === 0) { | |
| const $signin_button = document.getElementById(`signin_button`) | |
| if (!$signin_button) { | |
| if (attempt > 10) { | |
| const msg = `[ACTION:${action}] 已尝试 ${attempt} 次 (per 200ms),未找到 #signin_button 元素` | |
| alert(msg) | |
| throw new Error(msg) | |
| } | |
| setTimeout(() => { | |
| handleToLogin(attempt + 1) | |
| }, 200) | |
| return; | |
| } | |
| setInputValue(document.querySelector(`input[name="username"]`), context.username) | |
| setInputValue(document.querySelector(`input[name="password"]`), context.password) | |
| // 模拟点击按钮 | |
| $signin_button.click(); | |
| setTimeout(() => { | |
| handleToLogin(0, 1) | |
| }, 200) | |
| } else if (action === 1) { | |
| // 输入 OTP 页面 | |
| if (attempt > 10) { | |
| const msg = `[ACTION:${action}] 已尝试 ${attempt} 次 (per 200ms)` | |
| alert(msg) | |
| throw new Error(msg) | |
| } | |
| const $otp_input = document.querySelector(`input[name="mfaCode"]`) | |
| if (!$otp_input) { | |
| setTimeout(() => { | |
| handleToLogin(attempt + 1, 1) | |
| }, 200) | |
| return; | |
| } | |
| const otp = await generateTOTP(context.otopConfig.secret, { | |
| step: context.otopConfig.period, | |
| digits: context.otopConfig.digits, | |
| algorithm: context.otopConfig?.algorithm ?? 'SHA-1' | |
| }); | |
| setInputValue($otp_input, otp, ["input", "change"]); | |
| // 模拟点击 | |
| document.querySelector(`button[data-testid="mfa-submit-button"]`).click() | |
| } | |
| } | |
| function setInputValue(input, value, trigger = ["input"]) { | |
| const setter = Object.getOwnPropertyDescriptor( | |
| Object.getPrototypeOf(input), | |
| "value" | |
| )?.set; | |
| if (setter) { | |
| setter.call(input, value); // 最通用 | |
| } else { | |
| input.value = value; | |
| } | |
| trigger.forEach(eventName => { | |
| input.dispatchEvent(new Event(eventName, {bubbles: true})); | |
| }); | |
| } | |
| function base32Decode(input) { | |
| const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; | |
| const clean = input.replace(/=+$/, "").toUpperCase(); | |
| let bits = ""; | |
| for (let char of clean) { | |
| const val = alphabet.indexOf(char); | |
| if (val === -1) continue; | |
| bits += val.toString(2).padStart(5, "0"); | |
| } | |
| const bytes = []; | |
| for (let i = 0; i + 8 <= bits.length; i += 8) { | |
| bytes.push(parseInt(bits.slice(i, i + 8), 2)); | |
| } | |
| return new Uint8Array(bytes); | |
| } | |
| async function generateTOTP(secret, { | |
| step = 30, // Time step (default 30 seconds) | |
| digits = 6, // Output digits | |
| algorithm = "SHA-1" // SHA-1, SHA-256, SHA-512 | |
| } = {}) { | |
| const keyBytes = base32Decode(secret); | |
| const counter = Math.floor(Date.now() / 1000 / step); | |
| // Convert counter to 8-byte buffer (Big Endian) | |
| const buf = new ArrayBuffer(8); | |
| const view = new DataView(buf); | |
| view.setUint32(4, counter); | |
| const algorithmMapping = { | |
| "SHA1": "SHA-1", | |
| "SHA256": "SHA-256", | |
| "SHA512": "SHA-512" | |
| }; | |
| algorithm = algorithm.toUpperCase();// 转换为大写 | |
| // 重新映射 | |
| if (Object.keys(algorithmMapping).includes(algorithm)) { | |
| algorithm = algorithmMapping[algorithm]; | |
| console.log('Re mapping', algorithm); | |
| } | |
| // 再次判断 | |
| if (!["SHA-1", "SHA-256", "SHA512"].includes(algorithm)) { | |
| let message = `algorithm: ${algorithm} 不符合预期`; | |
| alert(message) | |
| throw new Error(message) | |
| } | |
| // Import key for HMAC | |
| const cryptoKey = await crypto.subtle.importKey( | |
| "raw", | |
| keyBytes, | |
| {name: "HMAC", hash: algorithm}, | |
| false, | |
| ["sign"] | |
| ); | |
| const hmac = new Uint8Array( | |
| await crypto.subtle.sign("HMAC", cryptoKey, buf) | |
| ); | |
| // Dynamic Truncation | |
| const offset = hmac[hmac.length - 1] & 0x0f; | |
| const binary = | |
| ((hmac[offset] & 0x7f) << 24) | | |
| ((hmac[offset + 1] & 0xff) << 16) | | |
| ((hmac[offset + 2] & 0xff) << 8) | | |
| (hmac[offset + 3] & 0xff); | |
| const otp = (binary % 10 ** digits).toString().padStart(digits, "0"); | |
| return otp; | |
| } | |
| function parseOtpauthURL(url) { | |
| const u = new URL(url); | |
| if (u.protocol !== "otpauth:") { | |
| throw new Error("Invalid otpauth URL"); | |
| } | |
| const type = u.hostname; // totp / hotp | |
| const label = decodeURIComponent(u.pathname.slice(1)); | |
| const params = Object.fromEntries(u.searchParams.entries()); | |
| return { | |
| type, | |
| label, | |
| secret: params.secret, | |
| issuer: params.issuer, | |
| algorithm: params.algorithm || "SHA-1", | |
| digits: params.digits ? Number(params.digits) : 6, | |
| period: params.period ? Number(params.period) : 30, | |
| }; | |
| } | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
A. 安装前提
安装 Tampermonkey:https://chromewebstore.google.com/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=en
打开扩展开发者模式 - 访问

chrome://extensions/配置插件 Tampermonkey - 允许用户脚本

重启浏览器 - 输入

chrome://restart按回车B. 正式安装插件
“OTPAuth URL” 是什么?
AWS 添加 OTP 时,会给你一个二维码,使用相关软件扫描添加,你需要把这张二维码图片保存下来,使用二维码解析工具解析得到 URL,这个就是
OTPAuth URL格式类似于:
otpauth://totp/Amazon%20Web%20Services:shine@123456789?secret=SECRET-SECRET-SECRET-SECRET&issuer=Amazon%20Web%20Services