From a994214e4a7ad9fd8407404140d0c4bfc08f672f Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Thu, 12 Mar 2026 01:59:28 +0800 Subject: [PATCH] refactor: optimize random byte generation for recovery and JWT secret functions --- src/utils/recovery-code.ts | 15 ++++++++------- webapp/src/components/JwtWarningPage.tsx | 11 ++++++++--- webapp/src/components/SettingsPage.tsx | 11 +++++++++-- webapp/src/components/VaultPage.tsx | 6 ------ webapp/src/lib/export-formats.ts | 4 ---- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/utils/recovery-code.ts b/src/utils/recovery-code.ts index 730a5b5..bdb906a 100644 --- a/src/utils/recovery-code.ts +++ b/src/utils/recovery-code.ts @@ -1,4 +1,6 @@ const RECOVERY_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; +const RECOVERY_ALPHABET_LENGTH = RECOVERY_ALPHABET.length; +const RECOVERY_MAX_UNBIASED_BYTE = Math.floor(256 / RECOVERY_ALPHABET_LENGTH) * RECOVERY_ALPHABET_LENGTH; function normalizeRecoveryCode(raw: string): string { return String(raw || '').toUpperCase().replace(/[^A-Z2-7]/g, ''); @@ -9,15 +11,14 @@ function formatRecoveryCode(compact: string): string { } export function createRecoveryCode(): string { - const bytes = crypto.getRandomValues(new Uint8Array(20)); let compact = ''; - for (const b of bytes) { - compact += RECOVERY_ALPHABET[b % RECOVERY_ALPHABET.length]; - } - // 20 bytes -> 20 chars in this simple mapping. Expand to 32 chars for friendlier grouping. while (compact.length < 32) { - const extra = crypto.getRandomValues(new Uint8Array(1))[0]; - compact += RECOVERY_ALPHABET[extra % RECOVERY_ALPHABET.length]; + const bytes = crypto.getRandomValues(new Uint8Array(32)); + for (const b of bytes) { + if (b >= RECOVERY_MAX_UNBIASED_BYTE) continue; + compact += RECOVERY_ALPHABET[b % RECOVERY_ALPHABET_LENGTH]; + if (compact.length >= 32) break; + } } return formatRecoveryCode(compact.slice(0, 32)); } diff --git a/webapp/src/components/JwtWarningPage.tsx b/webapp/src/components/JwtWarningPage.tsx index 280b834..9f2fe77 100644 --- a/webapp/src/components/JwtWarningPage.tsx +++ b/webapp/src/components/JwtWarningPage.tsx @@ -74,10 +74,15 @@ export default function JwtWarningPage(props: JwtWarningPageProps) { function generateJwtSecret(length: number): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; - const bytes = crypto.getRandomValues(new Uint8Array(length)); let out = ''; - for (let i = 0; i < length; i += 1) { - out += chars[bytes[i] % chars.length]; + const maxUnbiasedByte = Math.floor(256 / chars.length) * chars.length; + while (out.length < length) { + const bytes = crypto.getRandomValues(new Uint8Array(length)); + for (const value of bytes) { + if (value >= maxUnbiasedByte) continue; + out += chars[value % chars.length]; + if (out.length >= length) break; + } } return out; } diff --git a/webapp/src/components/SettingsPage.tsx b/webapp/src/components/SettingsPage.tsx index dc0393b..56fc7d2 100644 --- a/webapp/src/components/SettingsPage.tsx +++ b/webapp/src/components/SettingsPage.tsx @@ -16,9 +16,16 @@ interface SettingsPageProps { function randomBase32Secret(length: number): string { const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; - const random = crypto.getRandomValues(new Uint8Array(length)); let out = ''; - for (const x of random) out += alphabet[x % alphabet.length]; + const maxUnbiasedByte = Math.floor(256 / alphabet.length) * alphabet.length; + while (out.length < length) { + const random = crypto.getRandomValues(new Uint8Array(length)); + for (const x of random) { + if (x >= maxUnbiasedByte) continue; + out += alphabet[x % alphabet.length]; + if (out.length >= length) break; + } + } return out; } diff --git a/webapp/src/components/VaultPage.tsx b/webapp/src/components/VaultPage.tsx index 27ef9c7..87752cf 100644 --- a/webapp/src/components/VaultPage.tsx +++ b/webapp/src/components/VaultPage.tsx @@ -138,12 +138,6 @@ function parseFieldType(value: number | string | null | undefined): CustomFieldT return 0; } -function fieldTypeLabel(type: CustomFieldType): string { - if (type === 3) return t('txt_linked'); - const found = FIELD_TYPE_OPTIONS.find((x) => x.value === type); - return found ? found.label : t('txt_text'); -} - function toBooleanFieldValue(raw: string): boolean { const v = String(raw || '').trim().toLowerCase(); return v === '1' || v === 'true' || v === 'yes' || v === 'on'; diff --git a/webapp/src/lib/export-formats.ts b/webapp/src/lib/export-formats.ts index 6863c85..7e890df 100644 --- a/webapp/src/lib/export-formats.ts +++ b/webapp/src/lib/export-formats.ts @@ -112,10 +112,6 @@ function randomGuid(): string { return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; } -function toAesBuffer(bytes: Uint8Array): ArrayBuffer { - return new Uint8Array(bytes).buffer; -} - async function getCipherKeyParts(cipher: Cipher, userEnc: Uint8Array, userMac: Uint8Array): Promise<{ enc: Uint8Array; mac: Uint8Array }> { if (cipher.key && typeof cipher.key === 'string') { try {