Files
Laufzettel/src/lib/passwort.ts
T
florian.wilde eec5de404a
Prüfung / pruefung (push) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
feat(platzhalter): Aufloesung, Namensregeln und Shell-Maskierung (#26)
2026-09-25 13:30:31 +00:00

118 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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("");
}