Cryptographically secure generator for 5 types: - Password: configurable charset + entropy strength meter - UUID: crypto.randomUUID() - API Key: hex/base62/base64url with optional prefix - Hash: SHA-1/256/512 of custom input or random data - Token: variable byte-length in hex or base64url All using Web Crypto API — nothing leaves the browser. Registered in lib/tools.tsx with RandomIcon (dice). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
119 lines
3.1 KiB
TypeScript
119 lines
3.1 KiB
TypeScript
const CHARSET = {
|
|
uppercase: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
|
lowercase: 'abcdefghijklmnopqrstuvwxyz',
|
|
numbers: '0123456789',
|
|
symbols: '!@#$%^&*()-_=+[]{}|;:,.<>?',
|
|
hex: '0123456789abcdef',
|
|
base62: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
|
};
|
|
|
|
export interface PasswordOpts {
|
|
length: number;
|
|
uppercase: boolean;
|
|
lowercase: boolean;
|
|
numbers: boolean;
|
|
symbols: boolean;
|
|
}
|
|
|
|
export interface ApiKeyOpts {
|
|
length: number;
|
|
format: 'hex' | 'base62' | 'base64url';
|
|
prefix: string;
|
|
}
|
|
|
|
export interface HashOpts {
|
|
algorithm: 'SHA-1' | 'SHA-256' | 'SHA-512';
|
|
input: string;
|
|
}
|
|
|
|
export interface TokenOpts {
|
|
bytes: number;
|
|
format: 'hex' | 'base64url';
|
|
}
|
|
|
|
function randomBytes(n: number): Uint8Array {
|
|
const arr = new Uint8Array(n);
|
|
crypto.getRandomValues(arr);
|
|
return arr;
|
|
}
|
|
|
|
function toHex(bytes: Uint8Array): string {
|
|
return Array.from(bytes)
|
|
.map((b) => b.toString(16).padStart(2, '0'))
|
|
.join('');
|
|
}
|
|
|
|
function toBase64url(bytes: Uint8Array): string {
|
|
return btoa(String.fromCharCode(...bytes))
|
|
.replace(/\+/g, '-')
|
|
.replace(/\//g, '_')
|
|
.replace(/=/g, '');
|
|
}
|
|
|
|
export function generatePassword(opts: PasswordOpts): string {
|
|
let charset = '';
|
|
if (opts.uppercase) charset += CHARSET.uppercase;
|
|
if (opts.lowercase) charset += CHARSET.lowercase;
|
|
if (opts.numbers) charset += CHARSET.numbers;
|
|
if (opts.symbols) charset += CHARSET.symbols;
|
|
if (!charset) charset = CHARSET.lowercase + CHARSET.numbers;
|
|
|
|
const bytes = randomBytes(opts.length * 4);
|
|
let result = '';
|
|
let i = 0;
|
|
while (result.length < opts.length && i < bytes.length) {
|
|
const idx = bytes[i] % charset.length;
|
|
result += charset[idx];
|
|
i++;
|
|
}
|
|
return result.slice(0, opts.length);
|
|
}
|
|
|
|
export function passwordEntropy(opts: PasswordOpts): number {
|
|
let size = 0;
|
|
if (opts.uppercase) size += 26;
|
|
if (opts.lowercase) size += 26;
|
|
if (opts.numbers) size += 10;
|
|
if (opts.symbols) size += CHARSET.symbols.length;
|
|
if (size === 0) size = 36;
|
|
return Math.round(Math.log2(size) * opts.length);
|
|
}
|
|
|
|
export function generateUUID(): string {
|
|
return crypto.randomUUID();
|
|
}
|
|
|
|
export function generateApiKey(opts: ApiKeyOpts): string {
|
|
const bytes = randomBytes(opts.length * 2);
|
|
let key: string;
|
|
switch (opts.format) {
|
|
case 'hex':
|
|
key = toHex(bytes).slice(0, opts.length);
|
|
break;
|
|
case 'base64url':
|
|
key = toBase64url(bytes).slice(0, opts.length);
|
|
break;
|
|
case 'base62': {
|
|
const cs = CHARSET.base62;
|
|
key = Array.from(bytes)
|
|
.map((b) => cs[b % cs.length])
|
|
.join('')
|
|
.slice(0, opts.length);
|
|
break;
|
|
}
|
|
}
|
|
return opts.prefix ? `${opts.prefix}_${key}` : key;
|
|
}
|
|
|
|
export async function generateHash(opts: HashOpts): Promise<string> {
|
|
const data = opts.input.trim() || toHex(randomBytes(32));
|
|
const encoded = new TextEncoder().encode(data);
|
|
const hashBuffer = await crypto.subtle.digest(opts.algorithm, encoded);
|
|
return toHex(new Uint8Array(hashBuffer));
|
|
}
|
|
|
|
export function generateToken(opts: TokenOpts): string {
|
|
const bytes = randomBytes(opts.bytes);
|
|
return opts.format === 'hex' ? toHex(bytes) : toBase64url(bytes);
|
|
}
|