mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix: Fix checksum hashing when WebCrypto is unavailable (#23024)
This commit is contained in:
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
+3
-3
@@ -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, {
|
||||
|
||||
@@ -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<string> {
|
||||
const checksumPayload: Record<string, unknown> = {};
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user