feat(security): add RFC 3161 PDF timestamp tool (#5855)

Co-authored-by: Anthony Stirling <[email protected]>
This commit is contained in:
InstaZDLL
2026-03-24 17:00:33 +00:00
committed by GitHub
co-authored by Anthony Stirling
parent 7b3985e34a
commit 8bbfbd63d7
17 changed files with 1068 additions and 1 deletions
@@ -0,0 +1,80 @@
import { useEffect, useRef } from "react";
import { Stack, Text, Select } from "@mantine/core";
import { useTranslation } from "react-i18next";
import {
TimestampPdfParameters,
FALLBACK_TSA_PRESETS,
} from "@app/hooks/tools/timestampPdf/useTimestampPdfParameters";
import { useAppConfig } from "@app/contexts/AppConfigContext";
interface TimestampPdfSettingsProps {
parameters: TimestampPdfParameters;
onParameterChange: <K extends keyof TimestampPdfParameters>(
key: K,
value: TimestampPdfParameters[K]
) => void;
disabled?: boolean;
}
const TimestampPdfSettings = ({
parameters,
onParameterChange,
disabled = false,
}: TimestampPdfSettingsProps) => {
const { t } = useTranslation();
const { config } = useAppConfig();
const defaultApplied = useRef(false);
// Use backend presets (single source of truth) with fallback (TASK-10)
const presets = config?.timestampTsaPresets ?? FALLBACK_TSA_PRESETS;
// Build dropdown: presets + admin custom URLs, deduplicated (TASK-9)
const presetUrls = new Set(presets.map((p) => p.url));
const adminCustomUrls = (config?.timestampCustomTsaUrls ?? []).filter(
(url) => !presetUrls.has(url)
);
const selectData = [
...presets.map((preset) => ({ value: preset.url, label: preset.label })),
...adminCustomUrls.map((url) => ({ value: url, label: url })),
];
// Apply admin default TSA URL on first config load (TASK-2)
useEffect(() => {
if (!defaultApplied.current && config?.timestampDefaultTsaUrl) {
defaultApplied.current = true;
const adminDefault = config.timestampDefaultTsaUrl;
if (adminDefault && adminDefault !== parameters.tsaUrl) {
onParameterChange("tsaUrl", adminDefault);
}
}
}, [config?.timestampDefaultTsaUrl]);
return (
<Stack gap="md">
<Text size="sm" fw={500}>
{t("timestampPdf.options.title", "Timestamp Server (TSA)")}
</Text>
<Select
label={t("timestampPdf.options.tsaUrl.label", "Select a TSA server")}
description={t(
"timestampPdf.options.tsaUrl.desc",
"Pick a trusted Time Stamp Authority"
)}
data={selectData}
value={parameters.tsaUrl}
onChange={(value) => onParameterChange("tsaUrl", value ?? presets[0].url)}
disabled={disabled}
/>
<Text size="xs" c="dimmed">
{t(
"timestampPdf.options.note",
"Only a SHA-256 hash of your document is sent to the TSA server; the PDF file itself is never sent to the TSA server."
)}
</Text>
</Stack>
);
};
export default TimestampPdfSettings;
@@ -43,6 +43,7 @@ import FormFill from "@app/tools/formFill/FormFill";
import RemoveCertificateSign from "@app/tools/RemoveCertificateSign";
import RemoveImage from "@app/tools/RemoveImage";
import CertSign from "@app/tools/CertSign";
import TimestampPdf from "@app/tools/TimestampPdf";
import BookletImposition from "@app/tools/BookletImposition";
import Flatten from "@app/tools/Flatten";
import Rotate from "@app/tools/Rotate";
@@ -69,6 +70,7 @@ import { convertOperationConfig } from "@app/hooks/tools/convert/useConvertOpera
import { removeCertificateSignOperationConfig } from "@app/hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation";
import { changePermissionsOperationConfig } from "@app/hooks/tools/changePermissions/useChangePermissionsOperation";
import { certSignOperationConfig } from "@app/hooks/tools/certSign/useCertSignOperation";
import { timestampPdfOperationConfig } from "@app/hooks/tools/timestampPdf/useTimestampPdfOperation";
import { bookletImpositionOperationConfig } from "@app/hooks/tools/bookletImposition/useBookletImpositionOperation";
import { mergeOperationConfig } from '@app/hooks/tools/merge/useMergeOperation';
import { editTableOfContentsOperationConfig } from '@app/hooks/tools/editTableOfContents/useEditTableOfContentsOperation';
@@ -213,6 +215,19 @@ export function useTranslatedToolCatalog(): TranslatedToolCatalog {
operationConfig: certSignOperationConfig,
automationSettings: CertSignAutomationSettings,
},
timestampPdf: {
icon: <LocalIcon icon="schedule-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.timestampPdf.title", "Timestamp PDF"),
component: TimestampPdf,
description: t("home.timestampPdf.desc", "Add an RFC 3161 document timestamp to prove when your PDF existed"),
categoryId: ToolCategoryId.STANDARD_TOOLS,
subcategoryId: SubcategoryId.SIGNING,
maxFiles: -1,
endpoints: ["timestamp-pdf"],
operationConfig: timestampPdfOperationConfig,
automationSettings: null,
synonyms: getSynonyms(t, "timestampPdf"),
},
sign: {
icon: <LocalIcon icon="signature-rounded" width="1.5rem" height="1.5rem" />,
name: t("home.sign.title", "Sign"),
@@ -0,0 +1,33 @@
import { useTranslation } from 'react-i18next';
import { ToolType, useToolOperation } from '@app/hooks/tools/shared/useToolOperation';
import { createStandardErrorHandler } from '@app/utils/toolErrorHandler';
import { TimestampPdfParameters, defaultParameters } from '@app/hooks/tools/timestampPdf/useTimestampPdfParameters';
export const buildTimestampPdfFormData = (parameters: TimestampPdfParameters, file: File): FormData => {
const formData = new FormData();
formData.append('fileInput', file);
formData.append('tsaUrl', parameters.tsaUrl);
return formData;
};
export const timestampPdfOperationConfig = {
toolType: ToolType.singleFile,
buildFormData: buildTimestampPdfFormData,
operationType: 'timestampPdf',
endpoint: '/api/v1/security/timestamp-pdf',
multiFileEndpoint: false,
defaultParameters,
} as const;
export const useTimestampPdfOperation = () => {
const { t } = useTranslation();
return useToolOperation<TimestampPdfParameters>({
...timestampPdfOperationConfig,
getErrorMessage: createStandardErrorHandler(
t('timestampPdf.error.failed', 'An error occurred while timestamping the PDF.')
),
});
};
@@ -0,0 +1,33 @@
import { BaseParameters } from '@app/types/parameters';
import { useBaseParameters, BaseParametersHook } from '@app/hooks/tools/shared/useBaseParameters';
// Fallback presets used only when AppConfig hasn't loaded yet.
// The real presets are served by the backend via /api/v1/config/app-config
// (field: timestampTsaPresets) — single source of truth.
export const FALLBACK_TSA_PRESETS = [
{ label: 'DigiCert', url: 'http://timestamp.digicert.com' },
{ label: 'Sectigo', url: 'http://timestamp.sectigo.com' },
{ label: 'SSL.com', url: 'http://ts.ssl.com' },
{ label: 'FreeTSA', url: 'https://freetsa.org/tsr' },
{ label: 'MeSign', url: 'http://tsa.mesign.com' },
] as const;
export interface TimestampPdfParameters extends BaseParameters {
tsaUrl: string;
}
export const defaultParameters: TimestampPdfParameters = {
tsaUrl: FALLBACK_TSA_PRESETS[0].url,
};
export type TimestampPdfParametersHook = BaseParametersHook<TimestampPdfParameters>;
export const useTimestampPdfParameters = (): TimestampPdfParametersHook => {
return useBaseParameters({
defaultParameters,
endpointName: 'timestamp-pdf',
validateFn: (params) => {
return params.tsaUrl.trim().length > 0;
},
});
};
+60
View File
@@ -0,0 +1,60 @@
import { useTranslation } from "react-i18next";
import { createToolFlow } from "@app/components/tools/shared/createToolFlow";
import TimestampPdfSettings from "@app/components/tools/timestampPdf/TimestampPdfSettings";
import { useTimestampPdfParameters } from "@app/hooks/tools/timestampPdf/useTimestampPdfParameters";
import { useTimestampPdfOperation } from "@app/hooks/tools/timestampPdf/useTimestampPdfOperation";
import { useBaseTool } from "@app/hooks/tools/shared/useBaseTool";
import { BaseToolProps, ToolComponent } from "@app/types/tool";
const TimestampPdf = (props: BaseToolProps) => {
const { t } = useTranslation();
const base = useBaseTool(
"timestampPdf",
useTimestampPdfParameters,
useTimestampPdfOperation,
props
);
return createToolFlow({
files: {
selectedFiles: base.selectedFiles,
isCollapsed: base.hasResults,
},
steps: [
{
title: t("timestampPdf.steps.settings", "Settings"),
isCollapsed: base.settingsCollapsed,
onCollapsedClick: base.settingsCollapsed ? base.handleSettingsReset : undefined,
content: (
<TimestampPdfSettings
parameters={base.params.parameters}
onParameterChange={base.params.updateParameter}
disabled={base.endpointLoading}
/>
),
},
],
executeButton: {
text: t("timestampPdf.submit", "Apply Timestamp"),
isVisible: !base.hasResults,
loadingText: t("loading"),
onClick: base.handleExecute,
disabled:
!base.params.validateParameters() ||
!base.hasFiles ||
!base.endpointEnabled,
},
review: {
isVisible: base.hasResults,
operation: base.operation,
title: t("timestampPdf.results", "Timestamp Results"),
onFileClick: base.handleThumbnailClick,
onUndo: base.handleUndo,
},
});
};
TimestampPdf.tool = () => useTimestampPdfOperation;
export default TimestampPdf as ToolComponent;
+3
View File
@@ -50,6 +50,9 @@ export interface AppConfig {
googleDriveClientId?: string;
googleDriveApiKey?: string;
googleDriveAppId?: string;
timestampDefaultTsaUrl?: string;
timestampCustomTsaUrls?: string[];
timestampTsaPresets?: { label: string; url: string }[];
}
export type AppConfigBootstrapMode = 'blocking' | 'non-blocking';
+1
View File
@@ -52,6 +52,7 @@ export const CORE_REGULAR_TOOL_IDS = [
'overlayPdfs',
'getPdfInfo',
'validateSignature',
'timestampPdf',
'replaceColor',
'showJS',
'bookletImposition',
@@ -77,6 +77,7 @@ export const TOOL_CREDIT_COSTS: Record<ToolId, number> = {
convert: CREDIT_COSTS.LARGE,
ocr: CREDIT_COSTS.LARGE,
certSign: CREDIT_COSTS.LARGE,
timestampPdf: CREDIT_COSTS.LARGE,
// Extra large operations (10 credits)
automate: CREDIT_COSTS.XLARGE,