mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
fix: support steam totp code generation and formatting
Co-authored-by: shuaiplus <100134295+shuaiplus@users.noreply.github.com>
This commit is contained in:
committed by
Shuai
parent
010cda972c
commit
b5d58f1aa8
@@ -18,7 +18,9 @@ const TOTP_RING_CIRCUMFERENCE = 2 * Math.PI * TOTP_RING_RADIUS;
|
||||
const failedIconHosts = new Set<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)}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -235,7 +235,9 @@ export function maskSecret(value: 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)}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -167,19 +167,31 @@ export async function decryptStr(cipherString: string | null | undefined, encKey
|
||||
return new TextDecoder().decode(plain);
|
||||
}
|
||||
|
||||
export function extractTotpSecret(raw: string): string {
|
||||
if (!raw) return '';
|
||||
function normalizeTotpSecret(secret: string): string {
|
||||
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();
|
||||
if (!s) return '';
|
||||
if (!s) return { secret: '', steam: false };
|
||||
if (/^otpauth:\/\//i.test(s)) {
|
||||
try {
|
||||
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 {
|
||||
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 {
|
||||
@@ -202,7 +214,7 @@ function base32ToBytes(input: string): Uint8Array {
|
||||
}
|
||||
|
||||
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;
|
||||
const keyBytes = base32ToBytes(secret);
|
||||
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 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 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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user