diff --git a/public/locales/de/string.json b/public/locales/de/string.json index dbf0e8b..112e57e 100644 --- a/public/locales/de/string.json +++ b/public/locales/de/string.json @@ -84,7 +84,7 @@ "includeNumbers": "Zahlen einschließen (0-9)", "includeSymbols": "Sonderzeichen einschließen", "includeUppercase": "Großbuchstaben einschließen (A-Z)", - "lengthDesc": "Länge des Passwortes", + "lengthDesc": "Länge des Passwortes (>3 & <257)", "lengthPlaceholder": "z.B. 12", "optionsTitle": "Passwortoptionen", "resultTitle": "Generiertes Passwort", diff --git a/public/locales/en/string.json b/public/locales/en/string.json index 734d1e5..aa07e7b 100644 --- a/public/locales/en/string.json +++ b/public/locales/en/string.json @@ -84,7 +84,7 @@ "includeNumbers": "Include numbers (0-9)", "includeSymbols": "Include special characters", "includeUppercase": "Include uppercase letters (A-Z)", - "lengthDesc": "Length of the password", + "lengthDesc": "Length of the password (>3 & <257)", "lengthPlaceholder": "e.g. 12", "optionsTitle": "Password Options", "resultTitle": "Generated Password", diff --git a/public/locales/es/string.json b/public/locales/es/string.json index 217823b..d05af9b 100644 --- a/public/locales/es/string.json +++ b/public/locales/es/string.json @@ -84,7 +84,7 @@ "includeNumbers": "Incluir números (0-9)", "includeSymbols": "Incluir caracteres especiales", "includeUppercase": "Incluir letras mayúsculas (A-Z)", - "lengthDesc": "Longitud de la contraseña", + "lengthDesc": "Longitud de la contraseña (>3 & <257)", "lengthPlaceholder": "p. ej. 12", "optionsTitle": "Opciones de contraseña", "resultTitle": "Contraseña generada", diff --git a/public/locales/fr/string.json b/public/locales/fr/string.json index 5dfeb88..36877b4 100644 --- a/public/locales/fr/string.json +++ b/public/locales/fr/string.json @@ -84,7 +84,7 @@ "includeNumbers": "Inclure des chiffres (0-9)", "includeSymbols": "Inclure des caractères spéciaux", "includeUppercase": "Inclure des lettres majuscules (A-Z)", - "lengthDesc": "Longueur du mot de passe", + "lengthDesc": "Longueur du mot de passe (>3 & <257)", "lengthPlaceholder": "par exemple 12", "optionsTitle": "Options de mot de passe", "resultTitle": "Mot de passe généré", diff --git a/public/locales/hi/string.json b/public/locales/hi/string.json index 0482213..5e6b8c1 100644 --- a/public/locales/hi/string.json +++ b/public/locales/hi/string.json @@ -84,7 +84,7 @@ "includeNumbers": "संख्याएँ शामिल करें (0-9)", "includeSymbols": "विशेष वर्ण शामिल करें", "includeUppercase": "बड़े अक्षर (A-Z) शामिल करें", - "lengthDesc": "पासवर्ड की लंबाई", + "lengthDesc": "पासवर्ड की लंबाई (>3 & <257)", "lengthPlaceholder": "उदाहरणार्थ 12", "optionsTitle": "पासवर्ड विकल्प", "resultTitle": "जनरेट किया गया पासवर्ड", diff --git a/public/locales/ja/string.json b/public/locales/ja/string.json index ca7d31a..d4cfca2 100644 --- a/public/locales/ja/string.json +++ b/public/locales/ja/string.json @@ -84,7 +84,7 @@ "includeNumbers": "数字を含める(0~9)", "includeSymbols": "特殊文字を含める", "includeUppercase": "大文字(A~Z)を含める", - "lengthDesc": "パスワードの長さ", + "lengthDesc": "パスワードの長さ (>3 & <257)", "lengthPlaceholder": "例:12", "optionsTitle": "パスワードオプション", "resultTitle": "生成されたパスワード", diff --git a/public/locales/nl/string.json b/public/locales/nl/string.json index 53dd230..0e58278 100644 --- a/public/locales/nl/string.json +++ b/public/locales/nl/string.json @@ -84,7 +84,7 @@ "includeNumbers": "Inclusief cijfers (0-9)", "includeSymbols": "Speciale tekens toevoegen", "includeUppercase": "Inclusief hoofdletters (A-Z)", - "lengthDesc": "Lengte van het wachtwoord", + "lengthDesc": "Lengte van het wachtwoord (>3 & <257)", "lengthPlaceholder": "bijv. 12", "optionsTitle": "Wachtwoordopties", "resultTitle": "gegenereerd wachtwoord", diff --git a/public/locales/pt/string.json b/public/locales/pt/string.json index 8d10bae..e398a3a 100644 --- a/public/locales/pt/string.json +++ b/public/locales/pt/string.json @@ -84,7 +84,7 @@ "includeNumbers": "Incluir números (0-9)", "includeSymbols": "Incluir caracteres especiais", "includeUppercase": "Incluir letras maiúsculas (A-Z)", - "lengthDesc": "Comprimento da senha", + "lengthDesc": "Comprimento da senha (>3 & <257)", "lengthPlaceholder": "por exemplo 12", "optionsTitle": "Opções de senha", "resultTitle": "Senha gerada", diff --git a/public/locales/ru/string.json b/public/locales/ru/string.json index 714ae84..1d92124 100644 --- a/public/locales/ru/string.json +++ b/public/locales/ru/string.json @@ -84,7 +84,7 @@ "includeNumbers": "Включить цифры (0-9)", "includeSymbols": "Включить специальные символы", "includeUppercase": "Включайте заглавные буквы (A–Z)", - "lengthDesc": "Длина пароля", + "lengthDesc": "Длина пароля (>3 & <257)", "lengthPlaceholder": "например, 12", "optionsTitle": "Параметры пароля", "resultTitle": "Сгенерированный пароль", diff --git a/public/locales/zh/string.json b/public/locales/zh/string.json index 89593c6..856bd9f 100644 --- a/public/locales/zh/string.json +++ b/public/locales/zh/string.json @@ -84,7 +84,7 @@ "includeNumbers": "包含数字 (0-9)", "includeSymbols": "包含特殊字符", "includeUppercase": "包含大写字母(A-Z)", - "lengthDesc": "密码长度", + "lengthDesc": "密码长度 (>3 & <257)", "lengthPlaceholder": "例如 12", "optionsTitle": "密码选项", "resultTitle": "生成的密码", diff --git a/src/pages/tools/string/password-generator/index.tsx b/src/pages/tools/string/password-generator/index.tsx index 9d7f62d..253241a 100644 --- a/src/pages/tools/string/password-generator/index.tsx +++ b/src/pages/tools/string/password-generator/index.tsx @@ -77,8 +77,26 @@ export default function PasswordGenerator({ title }: ToolComponentProps) { description={t('passwordGenerator.lengthDesc')} placeholder={t('passwordGenerator.lengthPlaceholder')} value={values.length} - onOwnChange={(val) => updateField('length', val)} + onOwnChange={(val) => { + const length = Number(val); + + if (length > 256) { + updateField('length', '256'); + return; + } + + if (length < 4 && val !== '') { + updateField('length', '4'); + return; + } + + updateField('length', val); + }} type="number" + inputProps={{ + min: 4, + max: 256 + }} /> diff --git a/src/pages/tools/string/password-generator/password-generator.service.test.ts b/src/pages/tools/string/password-generator/password-generator.service.test.ts index e6bb634..042ab93 100644 --- a/src/pages/tools/string/password-generator/password-generator.service.test.ts +++ b/src/pages/tools/string/password-generator/password-generator.service.test.ts @@ -129,12 +129,6 @@ describe('generatePassword', () => { expect(result).toHaveLength(50); }); - it('should handle edge case with very short length', () => { - const options = { ...initialValues, length: '1' }; - const result = generatePassword(options); - expect(result).toHaveLength(1); - }); - it('should handle negative length', () => { const options = { ...initialValues, length: '-5' }; const result = generatePassword(options); diff --git a/src/pages/tools/string/password-generator/service.ts b/src/pages/tools/string/password-generator/service.ts index fe20a6b..35f7235 100644 --- a/src/pages/tools/string/password-generator/service.ts +++ b/src/pages/tools/string/password-generator/service.ts @@ -1,38 +1,75 @@ import type { InitialValuesType } from './initialValues'; +const LOWERCASE = 'abcdefghijklmnopqrstuvwxyz'; +const UPPERCASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +const NUMBERS = '0123456789'; +const SYMBOLS = '!@#$%^&*()_+~`|}{[]:;?><,./-='; + +const AMBIGUOUS = new Set(['i', 'I', 'l', '0', 'O']); + +const MAX_PASSWORD_LENGTH = 256; + +function randomChar(chars: string): string { + const array = new Uint32Array(1); + globalThis.crypto.getRandomValues(array); + + return chars[array[0] % chars.length]; +} + +function shuffle(array: string[]): void { + for (let i = array.length - 1; i > 0; i--) { + const random = new Uint32Array(1); + globalThis.crypto.getRandomValues(random); + + const j = random[0] % (i + 1); + + [array[i], array[j]] = [array[j], array[i]]; + } +} + +function removeAmbiguous(chars: string): string { + return [...chars].filter((char) => !AMBIGUOUS.has(char)).join(''); +} + export function generatePassword(options: InitialValuesType): string { - const length = parseInt(options.length || '', 10); - if (isNaN(length) || length <= 0) { + const length = Number.parseInt(options.length || '', 10); + + if (Number.isNaN(length) || length <= 0 || length > MAX_PASSWORD_LENGTH) { return ''; } - let charset = ''; - const lower = 'abcdefghijklmnopqrstuvwxyz'; - const upper = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; - const numbers = '0123456789'; - const symbols = '!@#$%^&*()_+~`|}{[]:;?><,./-='; - - if (options.includeLowercase) charset += lower; - if (options.includeUppercase) charset += upper; - if (options.includeNumbers) charset += numbers; - if (options.includeSymbols) charset += symbols; + let pools = [ + options.includeLowercase ? LOWERCASE : '', + options.includeUppercase ? UPPERCASE : '', + options.includeNumbers ? NUMBERS : '', + options.includeSymbols ? SYMBOLS : '' + ].filter(Boolean); if (options.avoidAmbiguous) { - // ambiguous set = i, I, l, 0, O - const ambig = new Set(['i', 'I', 'l', '0', 'O']); - charset = Array.from(charset) - .filter((c) => !ambig.has(c)) - .join(''); + pools = pools.map(removeAmbiguous); } - if (!charset) { - return ''; // nothing to pick from + // Remove empty pools after filtering + pools = pools.filter(Boolean); + + if (pools.length === 0 || length < pools.length) { + return ''; } - let pwd = ''; - for (let i = 0; i < length; i++) { - const idx = Math.floor(Math.random() * charset.length); - pwd += charset[idx]; + const password: string[] = []; + + // Guarantee each selected category appears once + for (const pool of pools) { + password.push(randomChar(pool)); } - return pwd; + + const charset = pools.join(''); + + while (password.length < length) { + password.push(randomChar(charset)); + } + + shuffle(password); + + return password.join(''); }