import { randomBytes, randomUUID } from "crypto"; import { execFileSync } from "child_process"; import { writeFileSync, mkdtempSync, readFileSync, rmSync } from "fs"; import { tmpdir } from "os"; import { join } from "path"; /** * Cooldown (Stunden) zwischen Entfern-Antrag und Sichtbarwerden des * Removal-Passworts. Schützt gegen impulsives Abschalten im Drang-Fenster. * Bewusst gleich dem Mobile-Device-Lock (24h) gehalten. */ export const MAGIC_RELEASE_COOLDOWN_H = 24; const PW_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // ohne 0/O/1/I/L const PW_LEN = 10; /** * Generiert ein menschenlesbares Removal-Passwort (Offboarding tippt es ab). * 10 Zeichen, keine mehrdeutigen Glyphen. ~50 Bit Entropie. */ export function generateRemovalPassword(): string { const bytes = randomBytes(PW_LEN); let out = ""; for (let i = 0; i < PW_LEN; i++) { out += PW_ALPHABET[bytes[i] % PW_ALPHABET.length]; } return out; } /** * Baut das com.apple.profileRemovalPassword-Payload-Dict (als XML-String), * das in die PayloadContent-Array des Mac/Win-Profils injiziert wird. */ export function buildRemovalPasswordPayload( password: string, deviceIdSlice: string, ): string { return ` PayloadDisplayName Entfern-Passwort PayloadIdentifier org.rebreak.protection.removalpw.${deviceIdSlice} PayloadType com.apple.profileRemovalPassword PayloadUUID ${randomUUID().toUpperCase()} PayloadVersion 1 RemovalPassword ${password} `; } /** * Signiert ein mobileconfig-Profil per CMS (openssl smime), WENN Signing-Cert * + Key via runtimeConfig konfiguriert sind. Sonst wird das Profil UNSIGNIERT * zurückgegeben — der Lock (RemovalPassword + PayloadRemovalDisallowed + * ProhibitDisablement) greift auch unsigniert; macOS zeigt nur „Nicht * verifiziert" statt grünem Häkchen. * * ⚠️ Inaktiv bis Cert auf der API-Box provisioniert ist (Pfade via Infisical). * Verifiziertes Signing-Setup existiert bereits auf der mdm-Box (LE-Cert für * dns.rebreak.org) — die hier erwarteten Pfade müssen dort/da verfügbar sein. */ export function signProfileIfConfigured( profileXml: string, signing: { certPath?: string; keyPath?: string; chainPath?: string } | undefined, ): Buffer | string { if (!signing?.certPath || !signing.keyPath) { return profileXml; // unsigniert — Lock greift trotzdem } const dir = mkdtempSync(join(tmpdir(), "rbk-sign-")); const inPath = join(dir, "in.mobileconfig"); const outPath = join(dir, "out.mobileconfig"); try { writeFileSync(inPath, profileXml, "utf8"); const args = [ "smime", "-sign", "-signer", signing.certPath, "-inkey", signing.keyPath, "-nodetach", "-outform", "der", "-in", inPath, "-out", outPath, ]; if (signing.chainPath) { args.push("-certfile", signing.chainPath); } execFileSync("openssl", args, { stdio: "pipe" }); return readFileSync(outPath); } finally { rmSync(dir, { recursive: true, force: true }); } }