fix: support steam totp code generation and formatting

Co-authored-by: shuaiplus <100134295+shuaiplus@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-03-17 03:39:02 +00:00
committed by Shuai
parent 010cda972c
commit b5d58f1aa8
3 changed files with 35 additions and 10 deletions
+3 -1
View File
@@ -18,7 +18,9 @@ const TOTP_RING_CIRCUMFERENCE = 2 * Math.PI * TOTP_RING_RADIUS;
const failedIconHosts = new Set<string>(); const failedIconHosts = new Set<string>();
function formatTotp(code: string): string { function formatTotp(code: string): string {
if (!code || code.length < 6) return code; if (!code) return code;
if (code.length === 5) return `${code.slice(0, 2)} ${code.slice(2)}`;
if (code.length < 6) return code;
return `${code.slice(0, 3)} ${code.slice(3, 6)}`; return `${code.slice(0, 3)} ${code.slice(3, 6)}`;
} }
@@ -235,7 +235,9 @@ export function maskSecret(value: string): string {
} }
export function formatTotp(code: string): string { export function formatTotp(code: string): string {
if (!code || code.length < 6) return code; if (!code) return code;
if (code.length === 5) return `${code.slice(0, 2)} ${code.slice(2)}`;
if (code.length < 6) return code;
return `${code.slice(0, 3)} ${code.slice(3, 6)}`; return `${code.slice(0, 3)} ${code.slice(3, 6)}`;
} }
+29 -8
View File
@@ -167,19 +167,31 @@ export async function decryptStr(cipherString: string | null | undefined, encKey
return new TextDecoder().decode(plain); return new TextDecoder().decode(plain);
} }
export function extractTotpSecret(raw: string): string { function normalizeTotpSecret(secret: string): string {
if (!raw) return ''; return secret.toUpperCase().replace(/[\s-]/g, '').replace(/=+$/g, '');
}
function parseTotpConfig(raw: string): { secret: string; steam: boolean } {
if (!raw) return { secret: '', steam: false };
const s = raw.trim(); const s = raw.trim();
if (!s) return ''; if (!s) return { secret: '', steam: false };
if (/^otpauth:\/\//i.test(s)) { if (/^otpauth:\/\//i.test(s)) {
try { try {
const u = new URL(s); const u = new URL(s);
return (u.searchParams.get('secret') || '').toUpperCase().replace(/[\s-]/g, '').replace(/=+$/g, ''); const label = decodeURIComponent((u.pathname || '').replace(/^\/+/, '')).toLowerCase();
const issuer = (u.searchParams.get('issuer') || '').trim().toLowerCase();
const algorithm = (u.searchParams.get('algorithm') || '').trim().toLowerCase();
const steam = issuer === 'steam' || label.startsWith('steam:') || algorithm === 'steam';
return { secret: normalizeTotpSecret(u.searchParams.get('secret') || ''), steam };
} catch { } catch {
return ''; return { secret: '', steam: false };
} }
} }
return s.toUpperCase().replace(/[\s-]/g, '').replace(/=+$/g, ''); return { secret: normalizeTotpSecret(s), steam: false };
}
export function extractTotpSecret(raw: string): string {
return parseTotpConfig(raw).secret;
} }
function base32ToBytes(input: string): Uint8Array { function base32ToBytes(input: string): Uint8Array {
@@ -202,7 +214,7 @@ function base32ToBytes(input: string): Uint8Array {
} }
export async function calcTotpNow(rawSecret: string): Promise<{ code: string; remain: number } | null> { export async function calcTotpNow(rawSecret: string): Promise<{ code: string; remain: number } | null> {
const secret = extractTotpSecret(rawSecret); const { secret, steam } = parseTotpConfig(rawSecret);
if (!secret) return null; if (!secret) return null;
const keyBytes = base32ToBytes(secret); const keyBytes = base32ToBytes(secret);
if (!keyBytes.length) return null; if (!keyBytes.length) return null;
@@ -221,6 +233,15 @@ export async function calcTotpNow(rawSecret: string): Promise<{ code: string; re
const hs = new Uint8Array(await crypto.subtle.sign('HMAC', key, toBufferSource(message))); const hs = new Uint8Array(await crypto.subtle.sign('HMAC', key, toBufferSource(message)));
const offset = hs[hs.length - 1] & 0x0f; const offset = hs[hs.length - 1] & 0x0f;
const bin = ((hs[offset] & 0x7f) << 24) | ((hs[offset + 1] & 0xff) << 16) | ((hs[offset + 2] & 0xff) << 8) | (hs[offset + 3] & 0xff); const bin = ((hs[offset] & 0x7f) << 24) | ((hs[offset + 1] & 0xff) << 16) | ((hs[offset + 2] & 0xff) << 8) | (hs[offset + 3] & 0xff);
const code = (bin % 1000000).toString().padStart(6, '0'); let code = (bin % 1000000).toString().padStart(6, '0');
if (steam) {
const chars = '23456789BCDFGHJKMNPQRTVWXY';
let value = bin;
code = '';
for (let i = 0; i < 5; i += 1) {
code += chars[value % chars.length];
value = Math.floor(value / chars.length);
}
}
return { code, remain }; return { code, remain };
} }