feat(platzhalter): Aufloesung, Namensregeln und Shell-Maskierung (#26)
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s

This commit was merged in pull request #26.
This commit is contained in:
2026-09-25 13:30:31 +00:00
parent f85bc87324
commit eec5de404a
21 changed files with 4069 additions and 166 deletions
+117
View File
@@ -0,0 +1,117 @@
import { randomInt } from "node:crypto";
/**
* Das generierte Passwort (#012).
*
* `{{generiertes_passwort}}` wird **bei jeder Anzeige neu erzeugt und nie
* gespeichert** — nicht im `Schrittergebnis`, nicht im `Kommentar`, nicht im
* `Protokoll` und nicht in der Druckansicht. Das ist keine Bequemlichkeit,
* sondern die Zusage: ein archivierter Lauf ist ein Dokument, das Jahre liegt
* und von vielen gelesen wird. Ein Anfangspasswort darin waere ein Fundstueck.
*
* Die Folge muss man aushalten: wer den Befehl ausfuehrt und das Passwort nicht
* sofort weiterreicht, hat es verloren. Genau deshalb steht der Hinweis
* „Passwort sofort in den Passwortmanager übernehmen“ daneben, und genau deshalb
* ist ein zweiter Aufruf ein **anderes** Passwort — nicht dasselbe noch einmal.
*/
/**
* Zeichen ohne Verwechslungsgefahr.
*
* Weggelassen sind `I l 1 |`, `O 0` und `B 8`: das Passwort wird von einem
* Bildschirm abgelesen und in ein anderes Fenster getippt, oft von der Person,
* der das Konto gehoert. Ein Zeichen, das man falsch liest, kostet einen
* Anruf — und der Sicherheitsgewinn dieser paar Zeichen ist dagegen nichts.
*/
export const PASSWORT_GROSS = "ACDEFGHJKLMNPQRSTUVWXYZ";
export const PASSWORT_KLEIN = "abcdefghijkmnopqrstuvwxyz";
export const PASSWORT_ZIFFERN = "23456789";
/**
* Sonderzeichen, die in keiner der drei Shells etwas anrichten.
*
* Bewusst ohne `" ' $ \` \\ ; & | < > %`: das Passwort landet in einem Befehl,
* den jemand kopiert. Die Maskierung in `src/lib/shell.ts` faengt das zwar ab —
* aber ein Passwort, das nur wegen korrekter Maskierung funktioniert, ist eine
* Wette auf jeden kuenftigen Aufrufer. Was gar nicht erst drin ist, kann auch
* nichts zerbrechen.
*
* Auch **ohne `!`**: bei eingeschalteter verzoegerter Ersetzung
* (`setlocal enabledelayedexpansion`) frisst `cmd.exe` es still, und das Konto
* bekaeme ein anderes Passwort als angezeigt. Gefunden im Review zu #012 — der
* Kommentar behauptete vorher Sicherheit in einer CSV-Zelle, und `= + - @`
* stehen dort am Zellenanfang fuer eine Formel. Deshalb faengt ein Passwort
* auch nie mit einem Sonderzeichen an (siehe `passwortErzeugen`).
*/
export const PASSWORT_SONDER = "#*+-=?@_~";
const ALPHABET = PASSWORT_GROSS + PASSWORT_KLEIN + PASSWORT_ZIFFERN + PASSWORT_SONDER;
export const PASSWORT_LAENGE = 20;
/** Steht in der Druckansicht statt des Passworts. */
export const PASSWORT_IM_DRUCK = "[Passwort – nicht gespeichert]";
/** Der Satz, der neben jedem angezeigten Passwort steht. */
export const PASSWORT_HINWEIS = "Passwort sofort in den Passwortmanager übernehmen";
function zeichenAus(vorrat: string): string {
// `randomInt` aus node:crypto statt Math.random: gleichverteilt und aus einer
// Quelle, die fuer Geheimnisse gedacht ist.
return vorrat.charAt(randomInt(vorrat.length));
}
/**
* Erzeugt ein Passwort mit 20 Zeichen.
*
* Aus jeder der vier Gruppen kommt mindestens ein Zeichen — nicht aus
* Aberglauben, sondern weil die Passwortrichtlinie eines Active Directory
* ueblicherweise drei von vier verlangt und ein abgewiesenes `New-ADUser` den
* Techniker vor dem Kunden stehen laesst. Danach wird gemischt, damit die
* Gruppen nicht an festen Stellen stehen.
*/
export function passwortErzeugen(laenge: number = PASSWORT_LAENGE): string {
if (laenge < 4) {
throw new Error("Ein Passwort unter vier Zeichen kann nicht aus vier Gruppen bestehen.");
}
const zeichen = [
zeichenAus(PASSWORT_GROSS),
zeichenAus(PASSWORT_KLEIN),
zeichenAus(PASSWORT_ZIFFERN),
zeichenAus(PASSWORT_SONDER),
];
while (zeichen.length < laenge) {
zeichen.push(zeichenAus(ALPHABET));
}
// Fisher-Yates mit `randomInt`: ein `sort(() => Math.random() - 0.5)` mischt
// nachweislich ungleich und wuerde die vier Pflichtzeichen bevorzugt vorn
// lassen.
for (let i = zeichen.length - 1; i > 0; i -= 1) {
const j = randomInt(i + 1);
[zeichen[i], zeichen[j]] = [zeichen[j] as string, zeichen[i] as string];
}
/*
* Das erste Zeichen ist nie ein Sonderzeichen.
*
* `= + - @` am Anfang einer Zelle liest eine Tabellenkalkulation als Formel,
* und ein Passwort geht seinen Weg oft ueber Zwischenablage und Tabelle. Statt
* die vier zu streichen — sie erhoehen den Vorrat spuerbar — wandert ein
* fuehrendes Sonderzeichen nach hinten. Die Laenge und die vier Gruppen
* bleiben unberuehrt.
*/
const erstes = zeichen[0] as string;
if (PASSWORT_SONDER.includes(erstes)) {
const tausch = zeichen.findIndex((zeichenAnStelle, index) => {
return index > 0 && !PASSWORT_SONDER.includes(zeichenAnStelle as string);
});
if (tausch > 0) {
zeichen[0] = zeichen[tausch] as string;
zeichen[tausch] = erstes;
}
}
return zeichen.join("");
}