mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
86 lines
2.5 KiB
TypeScript
86 lines
2.5 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
const RECOVERY_KEY_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
export const RECOVERY_KEY_BYTE_LENGTH = 20;
|
|
export const RECOVERY_KEY_LENGTH = 32;
|
|
const RECOVERY_KEY_GROUP_SIZE = 4;
|
|
export const RECOVERY_KEY_SEPARATOR = '-';
|
|
export const RECOVERY_KEY_FORMATTED_LENGTH = RECOVERY_KEY_LENGTH + RECOVERY_KEY_LENGTH / RECOVERY_KEY_GROUP_SIZE - 1;
|
|
|
|
const RECOVERY_KEY_ALIASES: Readonly<Record<string, string>> = {
|
|
O: '0',
|
|
I: '1',
|
|
L: '1',
|
|
};
|
|
|
|
const IGNORED_INPUT_CHARACTERS = /[\s\u2010-\u2015\u2212-]/gu;
|
|
|
|
function mapInputCharacter(character: string): string | null {
|
|
const upper = character.toUpperCase();
|
|
const mapped = RECOVERY_KEY_ALIASES[upper] ?? upper;
|
|
return mapped.length === 1 && RECOVERY_KEY_ALPHABET.includes(mapped) ? mapped : null;
|
|
}
|
|
|
|
export function encodeRecoveryKey(bytes: Uint8Array): string {
|
|
if (bytes.length !== RECOVERY_KEY_BYTE_LENGTH) {
|
|
throw new Error(`Recovery key needs exactly ${RECOVERY_KEY_BYTE_LENGTH} bytes`);
|
|
}
|
|
let output = '';
|
|
let buffer = 0;
|
|
let bits = 0;
|
|
for (const byte of bytes) {
|
|
buffer = (buffer << 8) | byte;
|
|
bits += 8;
|
|
while (bits >= 5) {
|
|
bits -= 5;
|
|
output += RECOVERY_KEY_ALPHABET[(buffer >> bits) & 31];
|
|
}
|
|
buffer &= (1 << bits) - 1;
|
|
}
|
|
return output;
|
|
}
|
|
|
|
export function formatRecoveryKey(key: string): string {
|
|
const groups: Array<string> = [];
|
|
for (let index = 0; index < key.length; index += RECOVERY_KEY_GROUP_SIZE) {
|
|
groups.push(key.slice(index, index + RECOVERY_KEY_GROUP_SIZE));
|
|
}
|
|
return groups.join(RECOVERY_KEY_SEPARATOR);
|
|
}
|
|
|
|
export function generateRecoveryKey(randomBytes: Uint8Array): {key: string; formatted: string} {
|
|
const key = encodeRecoveryKey(randomBytes);
|
|
return {key, formatted: formatRecoveryKey(key)};
|
|
}
|
|
|
|
export function normalizeRecoveryKey(input: string): string | null {
|
|
const stripped = input.replace(IGNORED_INPUT_CHARACTERS, '');
|
|
if (stripped.length !== RECOVERY_KEY_LENGTH) {
|
|
return null;
|
|
}
|
|
let normalized = '';
|
|
for (const character of stripped) {
|
|
const mapped = mapInputCharacter(character);
|
|
if (mapped === null) {
|
|
return null;
|
|
}
|
|
normalized += mapped;
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
export function formatRecoveryKeyInput(input: string): string {
|
|
let normalized = '';
|
|
for (const character of input.replace(IGNORED_INPUT_CHARACTERS, '')) {
|
|
const mapped = mapInputCharacter(character);
|
|
if (mapped === null) {
|
|
continue;
|
|
}
|
|
normalized += mapped;
|
|
if (normalized.length >= RECOVERY_KEY_LENGTH) {
|
|
break;
|
|
}
|
|
}
|
|
return formatRecoveryKey(normalized);
|
|
}
|