From 5ee15d41298b696c910027b2fd02680813919c66 Mon Sep 17 00:00:00 2001 From: Yauheni Balashka Date: Wed, 28 Oct 2026 14:46:04 +0300 Subject: [PATCH] FIO-11536: Fix clone encryption crash on Node 22+ --- src/Cloner.js | 32 ++++++++++++++++++++++++++++++-- test/encryption.js | 35 +++++++++++++++++++++++++++++++++++ test/index.js | 2 ++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 test/encryption.js diff --git a/src/Cloner.js b/src/Cloner.js index 8ce82fe..d58e77b 100644 --- a/src/Cloner.js +++ b/src/Cloner.js @@ -9,6 +9,32 @@ const {eachComponentAsync} = require('./eachComponentAsync'); const {createHmac} = require('node:crypto'); const fetch = require('./fetch'); +// Same key/IV derivation as the removed crypto.createCipher (OpenSSL EVP_BytesToKey, MD5), +// so data stays compatible with formio-server. +function deriveKeyAndIv(password) { + const keyLength = 32; + const ivLength = 16; + const passwordBuffer = Buffer.from(password, 'utf-8'); + const blocks = []; + let currentHash = Buffer.alloc(0); + let bytesGenerated = 0; + + while (bytesGenerated < keyLength + ivLength) { + const hash = crypto.createHash('md5'); + hash.update(currentHash); + hash.update(passwordBuffer); + currentHash = hash.digest(); + blocks.push(currentHash); + bytesGenerated += currentHash.length; + } + + const derivedBytes = Buffer.concat(blocks, keyLength + ivLength); + return { + key: derivedBytes.subarray(0, keyLength), + iv: derivedBytes.subarray(keyLength) + }; +} + class Cloner { /** * @param {*} srcPath - The source connection string. @@ -93,7 +119,8 @@ class Cloner { const salt = nosalt ? '' : keygenerator._({ length: this.defaultSaltLength }); - const cipher = crypto.createCipher('aes-256-cbc', secret); + const {key, iv} = deriveKeyAndIv(secret); + const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); const decryptedJSON = JSON.stringify(rawData) + salt; return Buffer.concat([ cipher.update(decryptedJSON), @@ -115,7 +142,8 @@ class Cloner { let data = {}; try { const buffer = Buffer.isBuffer(cipherbuffer) ? cipherbuffer : cipherbuffer.buffer; - const decipher = crypto.createDecipher('aes-256-cbc', secret); + const {key, iv} = deriveKeyAndIv(secret); + const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); const decryptedJSON = Buffer.concat([ decipher.update(buffer), // Buffer contains encrypted utf8 decipher.final() diff --git a/test/encryption.js b/test/encryption.js new file mode 100644 index 0000000..8ad8f7e --- /dev/null +++ b/test/encryption.js @@ -0,0 +1,35 @@ +/* globals describe, it, before */ +'use strict'; +const assert = require('assert'); +const Cloner = require('../src/Cloner'); + +// Ciphertexts produced by the legacy crypto.createCipher (Node 20), the format formio-server stores. +const legacy = { + settings: 'hg5cd/GcUiStAlrLtX6UErtXfOLBg4UnwjtV1+bOEj9hxSGQpCzH3ZbcHWYFrcIt6Z+Lr7sUA12hA1AqUhiAtw==', + field: 'ucekdmCGIwoJPYKfiFsLIU3gUOInqWh5Ul+BjUoBdqx4QXeg9T1mBbPELfDpr9cIQWhaHdeiW5qk07x5nRLrNw==' +}; +const settings = {secret: 'projectSecret', email: {smtp: {pass: 'smtpPass'}}}; + +describe('Cloner encryption', function() { + let cloner = null; + before(() => { + cloner = new Cloner('mongodb://src', 'mongodb://dst'); + }); + + it('decrypts project settings encrypted by formio-server', () => { + assert.deepEqual(cloner.decrypt('dbSecret', Buffer.from(legacy.settings, 'base64'), true), settings); + }); + + it('decrypts a salted field value encrypted by formio-server', () => { + assert.equal(cloner.decrypt('projectSecret', Buffer.from(legacy.field, 'base64')), '123-45-6789'); + }); + + it('encrypts project settings into the format formio-server reads', () => { + assert.equal(cloner.encrypt('dbSecret', settings, true).toString('base64'), legacy.settings); + }); + + it('round-trips a salted field value', () => { + const encrypted = cloner.encrypt('projectSecret', '123-45-6789'); + assert.equal(cloner.decrypt('projectSecret', encrypted), '123-45-6789'); + }); +}); diff --git a/test/index.js b/test/index.js index d35f590..00637f4 100644 --- a/test/index.js +++ b/test/index.js @@ -21,6 +21,8 @@ describe('Start tests', () => { require('./clearData')(process.env.MONGO_SRC, process.env.MONGO_DST); require('./createTemplate')(template); + require('./encryption'); + describe('CLI commands', () => { require('./submissions/submission')(template); require('./migrate/migrate')(template);