286 lines
7.4 KiB
Svelte
286 lines
7.4 KiB
Svelte
<svelte:head>
|
|
<title>API Key Generator</title>
|
|
</svelte:head>
|
|
|
|
<script lang="js">
|
|
import { copy } from '$lib/clipboard.js';
|
|
|
|
const ALPHABETS = {
|
|
hex: '0123456789abcdef',
|
|
base62: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
|
|
base58: '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz',
|
|
alphanum: '0123456789abcdefghijklmnopqrstuvwxyz',
|
|
alpha: 'abcdefghijklmnopqrstuvwxyz',
|
|
};
|
|
|
|
/**
|
|
* Generate cryptographically secure random string from alphabet.
|
|
* Uses rejection sampling to avoid modulo bias.
|
|
* @param {string} alphabet
|
|
* @param {number} length
|
|
* @returns {string}
|
|
*/
|
|
function randomString(alphabet, length) {
|
|
const chars = [];
|
|
// Find largest multiple of alphabet.length that fits in a byte (rejection sampling)
|
|
const max = 256 - (256 % alphabet.length);
|
|
while (chars.length < length) {
|
|
const bytes = crypto.getRandomValues(new Uint8Array(length * 2));
|
|
for (const b of bytes) {
|
|
if (b < max) {
|
|
chars.push(alphabet[b % alphabet.length]);
|
|
if (chars.length === length) break;
|
|
}
|
|
}
|
|
}
|
|
return chars.join('');
|
|
}
|
|
|
|
/**
|
|
* Generate raw bytes as base64url (no padding).
|
|
* @param {number} byteCount
|
|
* @returns {string}
|
|
*/
|
|
function randomBase64url(byteCount) {
|
|
const bytes = crypto.getRandomValues(new Uint8Array(byteCount));
|
|
const base64 = btoa(String.fromCharCode(...bytes));
|
|
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
}
|
|
|
|
/**
|
|
* @param {number} bits
|
|
* @returns {string}
|
|
*/
|
|
function randomHex(bits) {
|
|
const bytes = crypto.getRandomValues(new Uint8Array(bits / 8));
|
|
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
/**
|
|
* @typedef {{ id: string, label: string, desc: string }} Format
|
|
*/
|
|
|
|
/** @type {Format[]} */
|
|
const formats = [
|
|
{ id: 'hex128', label: 'hex-128', desc: '128-bit hex (32 chars)' },
|
|
{ id: 'hex256', label: 'hex-256', desc: '256-bit hex (64 chars)' },
|
|
{ id: 'base64url', label: 'base64url', desc: '256-bit base64url (43 chars)' },
|
|
{ id: 'base62_32', label: 'base62-32', desc: '32 chars base62 (~190 bits)' },
|
|
{ id: 'base62_43', label: 'base62-43', desc: '43 chars base62 (~256 bits)' },
|
|
{ id: 'base58_32', label: 'base58-32', desc: '32 chars base58 (~187 bits)' },
|
|
{ id: 'alphanum_32',label: 'alnum-32', desc: '32 lowercase alphanumeric (~165 bits)' },
|
|
{ id: 'custom', label: 'custom', desc: 'custom alphabet & length' },
|
|
];
|
|
|
|
/** @param {string} id @param {string} alphabet @param {number} customLen @returns {string} */
|
|
function generate(id, alphabet, customLen) {
|
|
switch (id) {
|
|
case 'hex128': return randomHex(128);
|
|
case 'hex256': return randomHex(256);
|
|
case 'base64url': return randomBase64url(32);
|
|
case 'base62_32': return randomString(ALPHABETS.base62, 32);
|
|
case 'base62_43': return randomString(ALPHABETS.base62, 43);
|
|
case 'base58_32': return randomString(ALPHABETS.base58, 32);
|
|
case 'alphanum_32': return randomString(ALPHABETS.alphanum, 32);
|
|
case 'custom': return randomString(alphabet || ALPHABETS.base62, customLen);
|
|
default: return '';
|
|
}
|
|
}
|
|
|
|
/** @param {string} alphabet @param {number} length @returns {number} */
|
|
const entropy = (alphabet, length) => Math.floor(Math.log2(alphabet.length) * length);
|
|
|
|
/** @param {string} id @param {string} customAlphabet @param {number} customLen @returns {number} */
|
|
function entropyFor(id, customAlphabet, customLen) {
|
|
switch (id) {
|
|
case 'hex128': return 128;
|
|
case 'hex256': return 256;
|
|
case 'base64url': return 256;
|
|
case 'base62_32': return entropy(ALPHABETS.base62, 32);
|
|
case 'base62_43': return entropy(ALPHABETS.base62, 43);
|
|
case 'base58_32': return entropy(ALPHABETS.base58, 32);
|
|
case 'alphanum_32': return entropy(ALPHABETS.alphanum, 32);
|
|
case 'custom': return entropy(customAlphabet || ALPHABETS.base62, customLen);
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
// --- state ---
|
|
|
|
let selectedFormat = $state('hex256');
|
|
let prefix = $state('');
|
|
let customAlphabet = $state(ALPHABETS.base62);
|
|
let customLength = $state(32);
|
|
let batchCount = $state(5);
|
|
|
|
/** @type {{ key: string }[]} */
|
|
let keys = $state([]);
|
|
|
|
function regenerate() {
|
|
keys = Array.from({ length: batchCount }, () => ({
|
|
key: (prefix ? prefix + '_' : '') + generate(selectedFormat, customAlphabet, customLength),
|
|
}));
|
|
}
|
|
|
|
// generate on load
|
|
regenerate();
|
|
|
|
const currentEntropy = $derived(entropyFor(selectedFormat, customAlphabet, customLength));
|
|
</script>
|
|
|
|
<h1>API Key Generator</h1>
|
|
|
|
<h2>Format</h2>
|
|
<table class="formats">
|
|
<tbody>
|
|
{#each formats as f}
|
|
<tr>
|
|
<td>
|
|
<label>
|
|
<input type="radio" bind:group={selectedFormat} value={f.id} onchange={regenerate} />
|
|
{f.label}
|
|
</label>
|
|
</td>
|
|
<td class="desc">{f.desc}</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
|
|
{#if selectedFormat === 'custom'}
|
|
<div class="custom-opts">
|
|
<label>
|
|
Alphabet:
|
|
<input type="text" bind:value={customAlphabet} size="50" spellcheck="false" oninput={regenerate} />
|
|
<span class="dim">({customAlphabet.length} chars)</span>
|
|
</label>
|
|
<br>
|
|
<label>
|
|
Length:
|
|
<input type="number" bind:value={customLength} min="8" max="256" size="5" oninput={regenerate} />
|
|
</label>
|
|
<br>
|
|
<div class="quick-alpha">
|
|
{#each Object.entries(ALPHABETS) as [name, abc]}
|
|
<button onclick={() => { customAlphabet = abc; regenerate(); }}>{name}</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<h2>Options</h2>
|
|
<p>
|
|
<label>
|
|
Prefix (optional):
|
|
<input type="text" bind:value={prefix} size="16" placeholder="sk, pk, token…" spellcheck="false" oninput={regenerate} />
|
|
</label>
|
|
|
|
<label>
|
|
Count:
|
|
<input type="number" bind:value={batchCount} min="1" max="50" size="4" oninput={regenerate} />
|
|
</label>
|
|
</p>
|
|
|
|
<h2>Keys <span class="entropy">~{currentEntropy} bits of entropy</span></h2>
|
|
<div class="actions">
|
|
<button onclick={regenerate}>Regenerate all</button>
|
|
<button onclick={() => copy(keys.map(k => k.key).join('\n'))}>Copy all</button>
|
|
</div>
|
|
<table class="keys">
|
|
<tbody>
|
|
{#each keys as k, i}
|
|
<tr>
|
|
<td class="idx">{i + 1}</td>
|
|
<td><code>{k.key}</code></td>
|
|
<td>
|
|
<button onclick={() => {
|
|
k.key = (prefix ? prefix + '_' : '') + generate(selectedFormat, customAlphabet, customLength);
|
|
}}>new</button>
|
|
</td>
|
|
<td><button onclick={() => copy(k.key)}>copy</button></td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
|
|
<style>
|
|
.formats {
|
|
border-collapse: collapse;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.formats td {
|
|
padding: 2px 10px 2px 0;
|
|
}
|
|
|
|
.desc {
|
|
color: var(--fg-dim);
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
.custom-opts {
|
|
border: 1px solid var(--border);
|
|
padding: 8px 12px;
|
|
margin-bottom: 8px;
|
|
background: var(--bg-panel);
|
|
}
|
|
|
|
.custom-opts label {
|
|
display: block;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.quick-alpha {
|
|
display: flex;
|
|
gap: 4px;
|
|
flex-wrap: wrap;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.dim {
|
|
color: var(--fg-faint);
|
|
font-size: 0.85em;
|
|
}
|
|
|
|
.entropy {
|
|
font-size: 0.75em;
|
|
font-weight: normal;
|
|
color: var(--green);
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.keys {
|
|
border-collapse: collapse;
|
|
width: 100%;
|
|
}
|
|
|
|
.keys td {
|
|
padding: 2px 6px;
|
|
border: 1px solid var(--border-faint);
|
|
}
|
|
|
|
.keys code {
|
|
font-size: 0.95em;
|
|
word-break: break-all;
|
|
}
|
|
|
|
.idx {
|
|
color: var(--fg-faint);
|
|
font-size: 0.85em;
|
|
width: 1px;
|
|
white-space: nowrap;
|
|
text-align: right;
|
|
}
|
|
|
|
.keys td:nth-child(3),
|
|
.keys td:nth-child(4) {
|
|
width: 1px;
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|