feat(apiKey): add secure API key generation utility and tests

This commit is contained in:
Supra4E8C
2026-08-06 18:07:35 +08:00
parent 6e550fbe4e
commit cfa8f616a3
3 changed files with 45 additions and 7 deletions
@@ -5,6 +5,7 @@ import { Modal } from '@/components/ui/Modal';
import { useNotificationStore } from '@/stores';
import { copyToClipboard } from '@/utils/clipboard';
import { makeClientId } from '@/types/visualConfig';
import { generateSecureApiKey } from '@/utils/apiKey';
import { maskApiKey } from '@/utils/format';
import { isValidApiKeyCharset } from '@/utils/validation';
import styles from './Blocks.module.scss';
@@ -46,13 +47,6 @@ export const ApiKeysCardEditor = memo(function ApiKeysCardEditor({
const [inputValue, setInputValue] = useState('');
const [formError, setFormError] = useState('');
function generateSecureApiKey(): string {
const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const array = new Uint8Array(17);
crypto.getRandomValues(array);
return 'sk-' + Array.from(array, (b) => charset[b % charset.length]).join('');
}
const openAddModal = () => {
setEditingApiKeyId(null);
setInputValue('');
+27
View File
@@ -0,0 +1,27 @@
const API_KEY_PREFIX = 'sk-';
const API_KEY_RANDOM_LENGTH = 48;
const API_KEY_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const MAX_UNBIASED_BYTE = Math.floor(256 / API_KEY_CHARSET.length) * API_KEY_CHARSET.length;
/**
* Generates a cryptographically secure, uniformly distributed API key.
* The resulting key is 51 characters long: `sk-` plus 48 random characters.
*/
export function generateSecureApiKey(): string {
const characters: string[] = [];
while (characters.length < API_KEY_RANDOM_LENGTH) {
const remaining = API_KEY_RANDOM_LENGTH - characters.length;
const randomBytes = new Uint8Array(Math.ceil(remaining * 1.1));
globalThis.crypto.getRandomValues(randomBytes);
for (const byte of randomBytes) {
if (byte >= MAX_UNBIASED_BYTE) continue;
characters.push(API_KEY_CHARSET[byte % API_KEY_CHARSET.length]);
if (characters.length === API_KEY_RANDOM_LENGTH) break;
}
}
return `${API_KEY_PREFIX}${characters.join('')}`;
}
+17
View File
@@ -0,0 +1,17 @@
import { describe, expect, test } from 'bun:test';
import { generateSecureApiKey } from '../src/utils/apiKey';
describe('API key generation', () => {
test('generates a 51-character key with the expected prefix and charset', () => {
const apiKey = generateSecureApiKey();
expect(apiKey).toHaveLength(51);
expect(apiKey).toMatch(/^sk-[A-Za-z0-9]{48}$/);
});
test('generates distinct keys', () => {
const apiKeys = Array.from({ length: 100 }, () => generateSecureApiKey());
expect(new Set(apiKeys).size).toBe(apiKeys.length);
});
});