Add secure password generator

This commit is contained in:
2026-08-04 11:07:55 +02:00
parent 25da7d49a9
commit 0c9bf6758c
14 changed files with 497 additions and 39 deletions
+4 -3
View File
@@ -48,13 +48,13 @@ const savedPassword = renderToStaticMarkup(
<PasswordField value="" saved savedPlaceholder="Saved password configured" onValueChange={noop} />
);
assert(savedPassword.includes('placeholder="Saved password configured"'), "saved password placeholder is rendered");
assert(!savedPassword.includes("password-field-toggle"), "saved empty password does not show reveal button");
assert(!savedPassword.includes("password-field-action"), "saved empty password without generator does not show an action button");
const typedPassword = renderToStaticMarkup(
<PasswordField value="secret" saved savedPlaceholder="Saved password configured" onValueChange={noop} />
);
assert(typedPassword.includes('aria-label="i18n:govoplan-core.show_password.044b852f"'), "typed password shows reveal button");
assert(typedPassword.includes("password-field-toggle"), "typed password gets toggle class");
assert(typedPassword.includes("password-field-action"), "typed password gets a reveal action");
const translatedAddressInput = renderToStaticMarkup(
<PlatformLanguageProvider>
@@ -83,7 +83,8 @@ const savedCredentialPanel = renderToStaticMarkup(
assert(savedCredentialPanel.includes("Credentials"), "credential panel renders heading");
assert(savedCredentialPanel.includes('value="sender"'), "credential panel renders username");
assert(savedCredentialPanel.includes('placeholder="Saved credential"'), "credential panel renders saved password placeholder");
assert(!savedCredentialPanel.includes("password-field-toggle"), "saved empty credential does not show reveal button");
assert(savedCredentialPanel.includes('aria-label="i18n:govoplan-core.generate_password.bd5bede8"'), "credential fields opt into the password generator");
assert(!savedCredentialPanel.includes('aria-label="i18n:govoplan-core.show_password.044b852f"'), "saved empty credential does not show a reveal action");
const settingsPanel = renderToStaticMarkup(
<MailServerSettingsPanel
+66
View File
@@ -0,0 +1,66 @@
function assert(condition: unknown, message = "assertion failed"): asserts condition {
if (!condition) throw new Error(message);
}
import { renderToStaticMarkup } from "react-dom/server";
import PasswordField from "../src/components/PasswordField";
import {
DEFAULT_PASSWORD_GENERATOR_OPTIONS,
generateSecurePassword,
type SecureRandomFill
} from "../src/components/passwordGenerator";
import { PlatformLanguageProvider } from "../src/i18n/LanguageContext";
function sequenceFill(sequence: number[]): SecureRandomFill {
let index = 0;
return (values) => {
values[0] = sequence[index % sequence.length] >>> 0;
index += 1;
return values;
};
}
const password = generateSecurePassword(
DEFAULT_PASSWORD_GENERATOR_OPTIONS,
sequenceFill(Array.from({ length: 64 }, (_, index) => index * 7919))
);
assert(password.length === 20, "the requested password length is preserved");
assert(/[a-z]/.test(password), "selected lowercase characters are represented");
assert(/[A-Z]/.test(password), "selected uppercase characters are represented");
assert(/[0-9]/.test(password), "selected digits are represented");
assert(/[!#$%&*+\-=?@^_]/.test(password), "selected symbols are represented");
let rejectedDraws = 0;
const rejectionAware = generateSecurePassword(
{ length: 12, lowercase: true, uppercase: false, digits: false, symbols: false },
(values) => {
values[0] = rejectedDraws++ % 2 === 0 ? 0xffff_ffff : 7;
return values;
}
);
assert(rejectionAware.length === 12, "out-of-range random draws are rejected without shortening output");
assert(rejectedDraws > 12, "modulo-bias rejection consumes a replacement secure draw");
for (const [options, expected] of [
[{ ...DEFAULT_PASSWORD_GENERATOR_OPTIONS, length: 11 }, "between 12 and 128"],
[{ ...DEFAULT_PASSWORD_GENERATOR_OPTIONS, lowercase: false, uppercase: false, digits: false, symbols: false }, "at least one character set"]
] as const) {
let message = "";
try {
generateSecurePassword(options, sequenceFill([1]));
} catch (error) {
message = error instanceof Error ? error.message : String(error);
}
assert(message.includes(expected), `invalid generator options should explain ${expected}`);
}
const markup = renderToStaticMarkup(
<PlatformLanguageProvider>
<PasswordField value="" onValueChange={() => undefined} generator />
</PlatformLanguageProvider>
);
assert(markup.includes('aria-label="Generate password"'), "the opt-in generator action is accessible");
assert(markup.includes("lucide-dice-5"), "the familiar generator icon is used");
assert(!markup.includes("password-generator-dialog"), "the generator dialog stays closed until explicitly requested");
console.log("Password generator contract passed.");