diff --git a/web-next/lib/security-token.ts b/web-next/lib/security-token.ts index ef18c2a..1756055 100644 --- a/web-next/lib/security-token.ts +++ b/web-next/lib/security-token.ts @@ -11,7 +11,37 @@ export interface Result { tokenDate?: string } -/** Solicita un token de seguridad (6 caracteres) por email. 1 cada 7 días. */ +/** Letras A-Z y a-z, sin dígitos: el formato del token es `uRlPBR`. */ +const TOKEN_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' +const TOKEN_LENGTH = 6 + +/** + * Genera el token de seguridad: 6 letras al azar. + * + * Antes era `randomBytes(4).toString('base64url').slice(0, 6)`, portado tal cual + * del Django original (`secrets.token_urlsafe(4)[:6]`). Tenía dos taras: metía + * `-` y `_` en un código que se lee y se teclea desde un correo, y sobre todo 4 + * bytes son 32 bits mientras que 6 caracteres base64 codifican 36 → al último + * carácter solo le llegaban 2 bits reales y SIEMPRE salía `A`, `Q`, `g` o `w`. + * + * Muestreo por rechazo: 256 no es múltiplo de 52, así que `byte % 52` haría más + * probables las primeras letras del alfabeto. Descartando los bytes >= 208 + * (52*4, el múltiplo de 52 más alto que cabe en un byte) el reparto es uniforme. + */ +function generateSecurityToken(): string { + const limit = 256 - (256 % TOKEN_ALPHABET.length) + let token = '' + while (token.length < TOKEN_LENGTH) { + for (const byte of crypto.randomBytes(TOKEN_LENGTH)) { + if (byte >= limit) continue + token += TOKEN_ALPHABET[byte % TOKEN_ALPHABET.length] + if (token.length === TOKEN_LENGTH) break + } + } + return token +} + +/** Solicita un token de seguridad (6 letras) por email. 1 cada 7 días. */ export async function requestSecurityToken(session: SessionData, ip: string): Promise { const userId = session.accountId ?? 0 const email = session.bnetEmail ?? '' @@ -26,7 +56,7 @@ export async function requestSecurityToken(session: SessionData, ip: string): Pr if (days < 7) return { success: false, error: 'cooldown' } } - const token = crypto.randomBytes(4).toString('base64url').slice(0, 6) + const token = generateSecurityToken() const expiresAt = new Date(Date.now() + 7 * 86400_000) await db(DB.default).query('DELETE FROM securitytoken WHERE user_id = ?', [userId])