118 lines
4.7 KiB
TypeScript
118 lines
4.7 KiB
TypeScript
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("");
|
||
}
|