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(""); }