diff --git a/src/main/account-status-sanitizer.ts b/src/main/account-status-sanitizer.ts new file mode 100644 index 0000000..2f4a50f --- /dev/null +++ b/src/main/account-status-sanitizer.ts @@ -0,0 +1,113 @@ +import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys"; +import { parseMegaDebridAccounts } from "../shared/mega-debrid-accounts"; +import type { AppSettings, DebridAccountStatus } from "../shared/types"; + +const REDACTED = "[geschützt]"; + +function safeDecode(value: string): string { + try { + return decodeURIComponent(value); + } catch { + return ""; + } +} + +function addRedaction(values: Set, value: unknown): void { + if (typeof value !== "string") { + return; + } + const raw = value; + const trimmed = value.trim(); + for (const candidate of [raw, trimmed]) { + if (!candidate) { + continue; + } + values.add(candidate); + const encoded = encodeURIComponent(candidate); + values.add(encoded); + values.add(encoded.replace(/%20/g, "+")); + const decoded = safeDecode(candidate); + if (decoded) { + values.add(decoded); + } + } +} + +function getInputString(input: unknown, key: string): string | undefined { + if (!input || typeof input !== "object") { + return undefined; + } + const value = (input as Record)[key]; + return typeof value === "string" ? value : undefined; +} + +export function collectAccountStatusRedactionValues(settings?: AppSettings, input?: unknown): string[] { + const values = new Set(); + if (settings) { + addRedaction(values, settings.token); + addRedaction(values, settings.megaPassword); + addRedaction(values, settings.megaCredentials); + addRedaction(values, settings.megaDebridApiCredentials); + addRedaction(values, settings.megaDebridWebCredentials); + addRedaction(values, settings.bestToken); + addRedaction(values, settings.allDebridToken); + addRedaction(values, settings.ddownloadPassword); + addRedaction(values, settings.oneFichierApiKey); + addRedaction(values, settings.debridLinkApiKeys); + addRedaction(values, settings.linkSnappyPassword); + addRedaction(values, settings.archivePasswordList); + addRedaction(values, settings.notifyUrl); + for (const raw of [settings.megaCredentials, settings.megaDebridApiCredentials, settings.megaDebridWebCredentials]) { + for (const account of parseMegaDebridAccounts(raw, settings.megaPassword)) { + addRedaction(values, account.password); + addRedaction(values, `${account.login}:${account.password}`); + } + } + for (const key of parseDebridLinkApiKeys(settings.debridLinkApiKeys)) { + addRedaction(values, key.token); + } + } + const inputIdentity = getInputString(input, "identity"); + const inputSecret = getInputString(input, "secret"); + addRedaction(values, inputSecret); + if (inputIdentity && inputSecret) { + addRedaction(values, `${inputIdentity.trim()}:${inputSecret.trim()}`); + } + return [...values].filter(Boolean).sort((left, right) => right.length - left.length); +} + +export function sanitizeAccountStatusText(value: string, redactions: readonly string[]): string { + let result = value; + result = result.replace(/\b(?:Authorization|Proxy-Authorization)\s*:\s*[^\r\n]+/gi, (match) => { + const name = match.slice(0, match.indexOf(":")); + return `${name}: ${REDACTED}`; + }); + result = result.replace(/\b(?:Cookie|Set-Cookie)\s*:\s*[^\r\n]+/gi, (match) => { + const name = match.slice(0, match.indexOf(":")); + return `${name}: ${REDACTED}`; + }); + result = result.replace(/\b(?:X-Api-Key|Api-Key|X-Auth-Token|X-Access-Token|Access-Token|Private-Token)\s*:\s*[^\r\n]+/gi, (match) => { + const name = match.slice(0, match.indexOf(":")); + return `${name}: ${REDACTED}`; + }); + result = result.replace(/((?:[?&;\s,]|^)(?:password|pass|pwd|token|api[_-]?key|apikey|access[_-]?token|private[_-]?token|secret|session|cookie|(?:backup|archive)?[_ -]?passphrase|archive[_ -]?password)\s*[:=]\s*)[^&\s"')]+/gi, `$1${REDACTED}`); + for (const secret of redactions) { + result = result.split(secret).join(REDACTED); + } + return result; +} + +export function sanitizeDebridAccountStatus(status: DebridAccountStatus, redactions: readonly string[]): DebridAccountStatus { + return { + ...status, + accountId: sanitizeAccountStatusText(status.accountId, redactions), + label: sanitizeAccountStatusText(status.label, redactions), + maskedLogin: sanitizeAccountStatusText(status.maskedLogin, redactions), + email: status.email ? sanitizeAccountStatusText(status.email, redactions) : undefined, + message: sanitizeAccountStatusText(status.message, redactions) + }; +} + +export function sanitizeDebridAccountStatuses(statuses: readonly DebridAccountStatus[], redactions: readonly string[]): DebridAccountStatus[] { + return statuses.map((status) => sanitizeDebridAccountStatus(status, redactions)); +} diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index 71f07fc..183c053 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -35,6 +35,7 @@ import { parseMegaDebridAccounts } from "../shared/mega-debrid-accounts"; import { getMegaDebridAccountsForMode } from "../shared/mega-debrid-accounts"; import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys"; import { applyAccountCommand } from "./account-commands"; +import { collectAccountStatusRedactionValues, sanitizeDebridAccountStatus, sanitizeDebridAccountStatuses } from "./account-status-sanitizer"; import { createRendererState } from "./renderer-state"; import { parseCollectorInput } from "./link-parser"; import { configureLogger, getLogFilePath, logger } from "./logger"; @@ -484,6 +485,7 @@ export class AppController { public async executeAccountCommand(command: AccountCommand): Promise { const applied = applyAccountCommand(this.settings, command); let checkedStatus: DebridAccountStatus | null = null; + const redactions = collectAccountStatusRedactionValues(applied.settings, command); if (command.action !== "delete" && applied.response.accountId && (command.kind === "megadebrid-api" || command.kind === "megadebrid-web")) { const mode = command.kind === "megadebrid-web" ? "web" : "api"; const account = getMegaDebridAccountsForMode(applied.settings, mode).find((entry) => entry.id === applied.response.accountId); @@ -495,10 +497,11 @@ export class AppController { if (!key) throw new Error("Account-Payload ist ungültig"); checkedStatus = await checkDebridLinkKey(key); } + if (checkedStatus) { + checkedStatus = sanitizeDebridAccountStatus(checkedStatus, redactions); + } if (checkedStatus && !checkedStatus.valid) { - const submittedSecret = "secret" in command ? command.secret : undefined; - const safeMessage = submittedSecret ? checkedStatus.message.split(submittedSecret).join("[geschützt]") : checkedStatus.message; - throw new Error(safeMessage || "Zugangsdaten ungültig"); + throw new Error(checkedStatus.message || "Zugangsdaten ungültig"); } this.updateSettings(applied.settings); if (checkedStatus) this.manager.applyDebridAccountStatuses([checkedStatus]); @@ -508,6 +511,7 @@ export class AppController { } public async checkAccountCredentials(input: AccountCredentialCheckInput): Promise { + const redactions = collectAccountStatusRedactionValues(this.settings, input); if (input.kind === "megadebrid-api" || input.kind === "megadebrid-web") { const mode = input.kind === "megadebrid-web" ? "web" : "api"; const account = input.identity?.trim() && input.secret @@ -515,14 +519,14 @@ export class AppController { : getMegaDebridAccountsForMode(this.settings, mode).find((entry) => entry.id === input.accountId); if (!account) throw new Error("Account-Payload ist ungültig"); const status = await checkMegaDebridAccount(account); - return { ...status, message: input.secret ? status.message.split(input.secret).join("[geschützt]") : status.message }; + return sanitizeDebridAccountStatus(status, redactions); } const key = input.secret?.trim() ? parseDebridLinkApiKeys(input.secret)[0] : parseDebridLinkApiKeys(this.settings.debridLinkApiKeys).find((entry) => entry.id === input.accountId); if (!key) throw new Error("Account-Payload ist ungültig"); const status = await checkDebridLinkKey(key); - return { ...status, message: input.secret ? status.message.split(input.secret).join("[geschützt]") : status.message }; + return sanitizeDebridAccountStatus(status, redactions); } public resetProviderDailyUsage(provider: DebridProvider): AppSettings { @@ -586,7 +590,10 @@ export class AppController { } public async checkDebridAccounts(): Promise { - const statuses = await checkAllDebridAccounts(this.settings); + const statuses = sanitizeDebridAccountStatuses( + await checkAllDebridAccounts(this.settings), + collectAccountStatusRedactionValues(this.settings) + ); this.manager.applyDebridAccountStatuses(statuses); this.audit("INFO", "Debrid-Accounts geprueft", { total: statuses.length, diff --git a/src/main/renderer-state.ts b/src/main/renderer-state.ts index a574046..5d51449 100644 --- a/src/main/renderer-state.ts +++ b/src/main/renderer-state.ts @@ -1,6 +1,7 @@ import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys"; -import { getMegaDebridAccountsForMode, getMegaDebridDisabledAccountIdsForMode, parseMegaDebridAccounts } from "../shared/mega-debrid-accounts"; +import { getMegaDebridAccountsForMode, getMegaDebridDisabledAccountIdsForMode } from "../shared/mega-debrid-accounts"; import type { AppSettings, DebridAccountStatus, DebridProvider, RendererAccount, RendererAccountKind, RendererSettings } from "../shared/types"; +import { collectAccountStatusRedactionValues, sanitizeDebridAccountStatus } from "./account-status-sanitizer"; function maskValue(value: string, keepStart = 3, keepEnd = 3): string { const trimmed = value.trim(); @@ -13,45 +14,11 @@ function maskValue(value: string, keepStart = 3, keepEnd = 3): string { return `${trimmed.slice(0, keepStart)}${"*".repeat(Math.max(4, trimmed.length - keepStart - keepEnd))}${trimmed.slice(-keepEnd)}`; } -function getSecrets(settings: AppSettings): string[] { - const megaSecrets = [settings.megaCredentials, settings.megaDebridApiCredentials, settings.megaDebridWebCredentials] - .flatMap((credentials) => parseMegaDebridAccounts(credentials, settings.megaPassword)) - .map((account) => account.password); - const debridLinkSecrets = parseDebridLinkApiKeys(settings.debridLinkApiKeys).map((key) => key.token); - return [...new Set([ - settings.token, - settings.megaPassword, - settings.megaCredentials, - settings.megaDebridApiCredentials, - settings.megaDebridWebCredentials, - ...megaSecrets, - settings.bestToken, - settings.allDebridToken, - settings.ddownloadPassword, - settings.oneFichierApiKey, - settings.debridLinkApiKeys, - ...debridLinkSecrets, - settings.linkSnappyPassword, - settings.archivePasswordList, - settings.notifyUrl - ].map((value) => value.trim()).filter(Boolean))].sort((left, right) => right.length - left.length); -} - -function redact(value: string, secrets: readonly string[]): string { - return secrets.reduce((result, secret) => result.split(secret).join("[geschützt]"), value); -} - -function safeStatus(status: DebridAccountStatus | undefined, secrets: readonly string[]): DebridAccountStatus | null { +function safeStatus(status: DebridAccountStatus | undefined, redactions: readonly string[]): DebridAccountStatus | null { if (!status) { return null; } - return { - ...status, - label: redact(status.label, secrets), - maskedLogin: redact(status.maskedLogin, secrets), - email: status.email ? redact(status.email, secrets) : undefined, - message: redact(status.message, secrets) - }; + return sanitizeDebridAccountStatus(status, redactions); } function providerEnabled(settings: AppSettings, provider: DebridProvider): boolean { @@ -83,7 +50,7 @@ function singleAccount( export function createRendererAccounts(settings: AppSettings): RendererAccount[] { const accounts: RendererAccount[] = []; - const secrets = getSecrets(settings); + const redactions = collectAccountStatusRedactionValues(settings); if (settings.realDebridUseWebLogin || settings.token.trim()) { accounts.push(singleAccount( settings, @@ -111,7 +78,7 @@ export function createRendererAccounts(settings: AppSettings): RendererAccount[] dailyLimitBytes: settings.megaDebridAccountDailyLimitBytes[account.id] || 0, dailyUsageBytes: settings.megaDebridAccountDailyUsageBytes[account.id] || 0, totalUsageBytes: settings.megaDebridAccountTotalUsageBytes[account.id] || 0, - status: safeStatus(settings.debridAccountStatuses[account.id], secrets) + status: safeStatus(settings.debridAccountStatuses[account.id], redactions) }); } } @@ -153,7 +120,7 @@ export function createRendererAccounts(settings: AppSettings): RendererAccount[] dailyLimitBytes: settings.debridLinkApiKeyDailyLimitBytes[key.id] || 0, dailyUsageBytes: settings.debridLinkApiKeyDailyUsageBytes[key.id] || 0, totalUsageBytes: settings.debridLinkApiKeyTotalUsageBytes[key.id] || 0, - status: safeStatus(settings.debridAccountStatuses[key.id], secrets) + status: safeStatus(settings.debridAccountStatuses[key.id], redactions) }); } if (settings.linkSnappyLogin.trim() && settings.linkSnappyPassword) { @@ -164,6 +131,7 @@ export function createRendererAccounts(settings: AppSettings): RendererAccount[] export function createRendererSettings(settings: AppSettings): RendererSettings { const configuredProviders = [...new Set(createRendererAccounts(settings).map((account) => account.provider))]; + const redactions = collectAccountStatusRedactionValues(settings); return { language: settings.language, realDebridUseWebLogin: settings.realDebridUseWebLogin, @@ -250,7 +218,7 @@ export function createRendererSettings(settings: AppSettings): RendererSettings megaDebridAccountDailyLimitBytes: { ...settings.megaDebridAccountDailyLimitBytes }, megaDebridAccountDailyUsageBytes: { ...settings.megaDebridAccountDailyUsageBytes }, megaDebridAccountTotalUsageBytes: { ...settings.megaDebridAccountTotalUsageBytes }, - debridAccountStatuses: Object.fromEntries(Object.entries(settings.debridAccountStatuses).map(([id, status]) => [id, safeStatus(status, getSecrets(settings))])), + debridAccountStatuses: Object.fromEntries(Object.entries(settings.debridAccountStatuses).map(([id, status]) => [id, safeStatus(status, redactions)])), providerDailyUsageDay: settings.providerDailyUsageDay, scheduledStartEpochMs: settings.scheduledStartEpochMs } as RendererSettings; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index f52a2da..e735383 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -6141,13 +6141,13 @@ export function App(): ReactElement { <> {ki + 1}