From 34260fe4befe4bbebeaa02dc4b6e7fac92c69f5f Mon Sep 17 00:00:00 2001 From: Daria Date: Wed, 10 Dec 2025 15:22:49 +0200 Subject: [PATCH] fix: Fix checksum hashing when WebCrypto is unavailable (#23024) --- .../cli/src/workflows/workflows.controller.ts | 29 +++++++++++++------ packages/frontend/editor-ui/src/Interface.ts | 1 + .../src/app/components/WorkflowSettings.vue | 6 ++-- .../app/composables/useWorkflowActivate.ts | 13 +++++---- .../src/app/composables/useWorkflowHelpers.ts | 9 ++++-- .../src/app/composables/useWorkflowSaving.ts | 5 ++-- .../src/app/stores/workflows.store.ts | 22 ++++++++------ .../workflowHistory/views/WorkflowHistory.vue | 6 ++-- packages/workflow/src/workflow-checksum.ts | 18 ++++++++++-- 9 files changed, 72 insertions(+), 37 deletions(-) diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 027715ca254..c967b234919 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -35,7 +35,7 @@ import { PROJECT_OWNER_ROLE_SLUG } from '@n8n/permissions'; import { In, type FindOptionsRelations } from '@n8n/typeorm'; import axios from 'axios'; import express from 'express'; -import { UnexpectedError } from 'n8n-workflow'; +import { UnexpectedError, calculateWorkflowChecksum } from 'n8n-workflow'; import { v4 as uuid } from 'uuid'; import { BadRequestError } from '@/errors/response-errors/bad-request.error'; @@ -260,7 +260,9 @@ export class WorkflowsController { const scopes = await this.workflowService.getWorkflowScopes(req.user, savedWorkflow.id); - return { ...savedWorkflowWithMetaData, scopes }; + const checksum = await calculateWorkflowChecksum(savedWorkflow); + + return { ...savedWorkflowWithMetaData, scopes, checksum }; } @Get('/', { middlewares: listQueryMiddleware }) @@ -386,8 +388,9 @@ export class WorkflowsController { delete workflowWithMetaData.shared; const scopes = await this.workflowService.getWorkflowScopes(req.user, workflowId); + const checksum = await calculateWorkflowChecksum(workflow); - return { ...workflowWithMetaData, scopes }; + return { ...workflowWithMetaData, scopes, checksum }; } // sharing disabled @@ -414,8 +417,9 @@ export class WorkflowsController { } const scopes = await this.workflowService.getWorkflowScopes(req.user, workflowId); + const checksum = await calculateWorkflowChecksum(workflow); - return { ...workflow, scopes }; + return { ...workflow, scopes, checksum }; } @Patch('/:workflowId') @@ -455,8 +459,9 @@ export class WorkflowsController { }); const scopes = await this.workflowService.getWorkflowScopes(req.user, workflowId); + const checksum = await calculateWorkflowChecksum(updatedWorkflow); - return { ...updatedWorkflow, scopes }; + return { ...updatedWorkflow, scopes, checksum }; } @Delete('/:workflowId') @@ -494,7 +499,9 @@ export class WorkflowsController { ); } - return workflow; + const checksum = await calculateWorkflowChecksum(workflow); + + return { ...workflow, checksum }; } @Post('/:workflowId/unarchive') @@ -515,7 +522,9 @@ export class WorkflowsController { ); } - return workflow; + const checksum = await calculateWorkflowChecksum(workflow); + + return { ...workflow, checksum }; } @Post('/:workflowId/activate') @@ -536,8 +545,9 @@ export class WorkflowsController { }); const scopes = await this.workflowService.getWorkflowScopes(req.user, workflowId); + const checksum = await calculateWorkflowChecksum(workflow); - return { ...workflow, scopes }; + return { ...workflow, scopes, checksum }; } @Post('/:workflowId/deactivate') @@ -548,8 +558,9 @@ export class WorkflowsController { const workflow = await this.workflowService.deactivateWorkflow(req.user, workflowId); const scopes = await this.workflowService.getWorkflowScopes(req.user, workflowId); + const checksum = await calculateWorkflowChecksum(workflow); - return { ...workflow, scopes }; + return { ...workflow, scopes, checksum }; } @Post('/:workflowId/run') diff --git a/packages/frontend/editor-ui/src/Interface.ts b/packages/frontend/editor-ui/src/Interface.ts index dc9e21a9dd5..3f1bfeb4f26 100644 --- a/packages/frontend/editor-ui/src/Interface.ts +++ b/packages/frontend/editor-ui/src/Interface.ts @@ -262,6 +262,7 @@ export interface IWorkflowDb { updatedAt?: string; }; activeVersion?: WorkflowHistory | null; + checksum?: string; } // For workflow list we don't need the full workflow data diff --git a/packages/frontend/editor-ui/src/app/components/WorkflowSettings.vue b/packages/frontend/editor-ui/src/app/components/WorkflowSettings.vue index 339b4ccc091..ffc709aa79f 100644 --- a/packages/frontend/editor-ui/src/app/components/WorkflowSettings.vue +++ b/packages/frontend/editor-ui/src/app/components/WorkflowSettings.vue @@ -22,7 +22,7 @@ import { N8nTooltip, } from '@n8n/design-system'; import type { WorkflowSettings } from 'n8n-workflow'; -import { calculateWorkflowChecksum, deepCopy } from 'n8n-workflow'; +import { deepCopy } from 'n8n-workflow'; import { useSettingsStore } from '@/app/stores/settings.store'; import { useRootStore } from '@n8n/stores/useRootStore'; import { useWorkflowsEEStore } from '@/app/stores/workflows.ee.store'; @@ -483,7 +483,9 @@ const saveSettings = async () => { try { const workflowData = await workflowsStore.updateWorkflow(String(route.params.name), data); workflowsStore.setWorkflowVersionId(workflowData.versionId); - workflowsStore.setWorkflowChecksum(await calculateWorkflowChecksum(workflowData)); + if (workflowData.checksum) { + workflowsStore.setWorkflowChecksum(workflowData.checksum); + } } catch (error) { toast.showError(error, i18n.baseText('workflowSettings.showError.saveSettings3.title')); isLoading.value = false; diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowActivate.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowActivate.ts index 5a71f39c42e..45977a2cbf9 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowActivate.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowActivate.ts @@ -18,7 +18,6 @@ import { useNpsSurveyStore } from '@/app/stores/npsSurvey.store'; import { useWorkflowSaving } from './useWorkflowSaving'; import * as workflowsApi from '@/app/api/workflows'; import { useRootStore } from '@n8n/stores/useRootStore'; -import { calculateWorkflowChecksum } from 'n8n-workflow'; export function useWorkflowActivate() { const updatingWorkflowActivation = ref(false); @@ -116,8 +115,8 @@ export function useWorkflowActivate() { workflowsStore.setWorkflowInactive(currWorkflowId); } - if (isCurrentWorkflow) { - workflowsStore.setWorkflowChecksum(await calculateWorkflowChecksum(workflow)); + if (isCurrentWorkflow && workflow.checksum) { + workflowsStore.setWorkflowChecksum(workflow.checksum); } } catch (error) { const newStateName = newActiveState ? 'activated' : 'deactivated'; @@ -195,7 +194,9 @@ export function useWorkflowActivate() { if (workflowId === workflowsStore.workflowId) { workflowsStore.setWorkflowVersionId(updatedWorkflow.versionId); - workflowsStore.setWorkflowChecksum(await calculateWorkflowChecksum(updatedWorkflow)); + if (updatedWorkflow.checksum) { + workflowsStore.setWorkflowChecksum(updatedWorkflow.checksum); + } } void useExternalHooks().run('workflow.published', { @@ -243,8 +244,8 @@ export function useWorkflowActivate() { try { const updatedWorkflow = await workflowsStore.deactivateWorkflow(workflowId); - if (workflowId === workflowsStore.workflowId) { - workflowsStore.setWorkflowChecksum(await calculateWorkflowChecksum(updatedWorkflow)); + if (workflowId === workflowsStore.workflowId && updatedWorkflow.checksum) { + workflowsStore.setWorkflowChecksum(updatedWorkflow.checksum); } void useExternalHooks().run('workflow.unpublished', { diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts index 10014c4a804..c76f1ef63dd 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowHelpers.ts @@ -23,7 +23,6 @@ import type { Workflow, } from 'n8n-workflow'; import { - calculateWorkflowChecksum, CHAT_TRIGGER_NODE_TYPE, createEmptyRunExecutionData, FORM_TRIGGER_NODE_TYPE, @@ -849,7 +848,9 @@ export function useWorkflowHelpers() { const workflow = await workflowsStore.updateWorkflow(workflowId, data); workflowsStore.setWorkflowVersionId(workflow.versionId); - workflowsStore.setWorkflowChecksum(await calculateWorkflowChecksum(workflow)); + if (workflow.checksum) { + workflowsStore.setWorkflowChecksum(workflow.checksum); + } if (isCurrentWorkflow) { workflowState.setActive(workflow.activeVersionId); @@ -953,7 +954,9 @@ export function useWorkflowHelpers() { workflowState.setWorkflowSettings(workflowData.settings ?? {}); workflowsStore.setWorkflowPinData(workflowData.pinData ?? {}); workflowsStore.setWorkflowVersionId(workflowData.versionId); - workflowsStore.setWorkflowChecksum(await calculateWorkflowChecksum(workflowData)); + if (workflowData.checksum) { + workflowsStore.setWorkflowChecksum(workflowData.checksum); + } workflowsStore.setWorkflowMetadata(workflowData.meta); workflowsStore.setWorkflowScopes(workflowData.scopes); diff --git a/packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts b/packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts index 381eb364f69..3df492fd06a 100644 --- a/packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts +++ b/packages/frontend/editor-ui/src/app/composables/useWorkflowSaving.ts @@ -19,7 +19,6 @@ import { useCanvasStore } from '@/app/stores/canvas.store'; import type { IUpdateInformation, IWorkflowDb, NotificationOptions } from '@/Interface'; import type { ITag } from '@n8n/rest-api-client/api/tags'; import type { WorkflowDataCreate, WorkflowDataUpdate } from '@n8n/rest-api-client/api/workflows'; -import { calculateWorkflowChecksum } from 'n8n-workflow'; import type { IDataObject, INode, IWorkflowSettings } from 'n8n-workflow'; import { useNodeTypesStore } from '@/app/stores/nodeTypes.store'; import { useToast } from './useToast'; @@ -246,7 +245,9 @@ export function useWorkflowSaving({ forceSave, ); workflowsStore.setWorkflowVersionId(workflowData.versionId); - workflowsStore.setWorkflowChecksum(await calculateWorkflowChecksum(workflowData)); + if (workflowData.checksum) { + workflowsStore.setWorkflowChecksum(workflowData.checksum); + } if (name) { workflowState.setWorkflowName({ newName: workflowData.name, setStateDirty: false }); diff --git a/packages/frontend/editor-ui/src/app/stores/workflows.store.ts b/packages/frontend/editor-ui/src/app/stores/workflows.store.ts index 9c7785d1107..692df8f2291 100644 --- a/packages/frontend/editor-ui/src/app/stores/workflows.store.ts +++ b/packages/frontend/editor-ui/src/app/stores/workflows.store.ts @@ -55,7 +55,6 @@ import type { ITaskStartedData, } from 'n8n-workflow'; import { - calculateWorkflowChecksum, deepCopy, NodeConnectionTypes, SEND_AND_WAIT_OPERATION, @@ -755,8 +754,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { async function updateWorkflowChecksum() { const updatedWorkflow = await fetchWorkflow(workflow.value.id); - const checksum = await calculateWorkflowChecksum(updatedWorkflow); - setWorkflowChecksum(checksum); + if (updatedWorkflow.checksum) { + setWorkflowChecksum(updatedWorkflow.checksum); + } } function setWorkflowActiveVersion(version: WorkflowHistory) { @@ -865,8 +865,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { workflowsById.value[id].versionId = updatedWorkflow.versionId; } - if (id === workflow.value.id) { - setWorkflowChecksum(await calculateWorkflowChecksum(updatedWorkflow)); + if (id === workflow.value.id && updatedWorkflow.checksum) { + setWorkflowChecksum(updatedWorkflow.checksum); } setWorkflowInactive(id); @@ -889,8 +889,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { } // Update checksum if unarchiving the currently open workflow - if (id === workflow.value.id) { - setWorkflowChecksum(await calculateWorkflowChecksum(updatedWorkflow)); + if (id === workflow.value.id && updatedWorkflow.checksum) { + setWorkflowChecksum(updatedWorkflow.checksum); } if (id === workflow.value.id) { @@ -1718,7 +1718,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { // Update local store state to reflect the change if (isCurrentWorkflow) { setWorkflowVersionId(updated.versionId); - setWorkflowChecksum(await calculateWorkflowChecksum(updated)); + if (updated.checksum) { + setWorkflowChecksum(updated.checksum); + } setWorkflowSettings(updated.settings ?? {}); } else if (workflowsById.value[id]) { workflowsById.value[id] = { @@ -1772,7 +1774,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { if (updated.versionId !== currentVersionId) { setWorkflowVersionId(updated.versionId); } - setWorkflowChecksum(await calculateWorkflowChecksum(updated)); + if (updated.checksum) { + setWorkflowChecksum(updated.checksum); + } } return updated; diff --git a/packages/frontend/editor-ui/src/features/workflows/workflowHistory/views/WorkflowHistory.vue b/packages/frontend/editor-ui/src/features/workflows/workflowHistory/views/WorkflowHistory.vue index 6ca42242ea9..c9d15428ef3 100644 --- a/packages/frontend/editor-ui/src/features/workflows/workflowHistory/views/WorkflowHistory.vue +++ b/packages/frontend/editor-ui/src/features/workflows/workflowHistory/views/WorkflowHistory.vue @@ -27,7 +27,7 @@ import { useRootStore } from '@n8n/stores/useRootStore'; import { useWorkflowActivate } from '@/app/composables/useWorkflowActivate'; import { getResourcePermissions } from '@n8n/permissions'; import { usePageRedirectionHelper } from '@/app/composables/usePageRedirectionHelper'; -import { calculateWorkflowChecksum, type IUser } from 'n8n-workflow'; +import type { IUser } from 'n8n-workflow'; import { N8nBadge, N8nButton, N8nHeading } from '@n8n/design-system'; import { createEventBus } from '@n8n/utils/event-bus'; @@ -266,8 +266,8 @@ const restoreWorkflowVersion = async ( deactivateAndRestore, ); - if (workflowId.value === workflowsStore.workflowId) { - workflowsStore.setWorkflowChecksum(await calculateWorkflowChecksum(activeWorkflow.value)); + if (workflowId.value === workflowsStore.workflowId && activeWorkflow.value.checksum) { + workflowsStore.setWorkflowChecksum(activeWorkflow.value.checksum); } const history = await workflowHistoryStore.getWorkflowHistory(workflowId.value, { diff --git a/packages/workflow/src/workflow-checksum.ts b/packages/workflow/src/workflow-checksum.ts index f057b835bec..3fd1dca33e3 100644 --- a/packages/workflow/src/workflow-checksum.ts +++ b/packages/workflow/src/workflow-checksum.ts @@ -1,3 +1,5 @@ +import jsSHA from 'jssha'; + import type { IConnections, INode, IPinData, IWorkflowSettings } from './interfaces'; import { isObject } from './utils'; @@ -57,6 +59,9 @@ function sortObjectKeys(value: unknown): unknown { /** * Calculates SHA-256 checksum of workflow content fields for conflict detection. * Excludes: id, versionId, timestamps, staticData, relations. + * + * Uses WebCrypto when available (e.g. browser in secure context), and falls back to a pure-JS SHA-256 + * implementation to also work in environments where WebCrypto is unavailable (e.g. HTTP/insecure contexts). */ export async function calculateWorkflowChecksum(workflow: WorkflowSnapshot): Promise { const checksumPayload: Record = {}; @@ -71,9 +76,16 @@ export async function calculateWorkflowChecksum(workflow: WorkflowSnapshot): Pro const normalizedPayload = sortObjectKeys(checksumPayload); const serializedPayload = JSON.stringify(normalizedPayload); - const data = new TextEncoder().encode(serializedPayload); - const hashBuffer = await crypto.subtle.digest('SHA-256', data); - return arrayBufferToHex(hashBuffer); + const subtle = globalThis.crypto?.subtle; + if (subtle) { + const data = new TextEncoder().encode(serializedPayload); + const hashBuffer = await subtle.digest('SHA-256', data); + return arrayBufferToHex(hashBuffer); + } + + const shaObj = new jsSHA('SHA-256', 'TEXT', { encoding: 'UTF8' }); + shaObj.update(serializedPayload); + return shaObj.getHash('HEX').toLowerCase(); } function arrayBufferToHex(arrayBuffer: ArrayBuffer): string {