From cfa8f616a35ddf7afa1345dd94caaea7962d4afd Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Thu, 6 Aug 2026 18:07:35 +0800 Subject: [PATCH] feat(apiKey): add secure API key generation utility and tests --- .../components/blocks/ApiKeysCardEditor.tsx | 8 +----- src/utils/apiKey.ts | 27 +++++++++++++++++++ tests/apiKey.test.ts | 17 ++++++++++++ 3 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 src/utils/apiKey.ts create mode 100644 tests/apiKey.test.ts diff --git a/src/features/config/components/blocks/ApiKeysCardEditor.tsx b/src/features/config/components/blocks/ApiKeysCardEditor.tsx index 0d5a09e3..dd1a3939 100644 --- a/src/features/config/components/blocks/ApiKeysCardEditor.tsx +++ b/src/features/config/components/blocks/ApiKeysCardEditor.tsx @@ -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(''); diff --git a/src/utils/apiKey.ts b/src/utils/apiKey.ts new file mode 100644 index 00000000..c827a9ae --- /dev/null +++ b/src/utils/apiKey.ts @@ -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('')}`; +} diff --git a/tests/apiKey.test.ts b/tests/apiKey.test.ts new file mode 100644 index 00000000..16c489e9 --- /dev/null +++ b/tests/apiKey.test.ts @@ -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); + }); +});