crypto.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. /**
  2. * @node crypto
  3. * @name Crypto
  4. * @category crypto
  5. * @version 1.1.0
  6. * @description Hash data, generate HMACs, encrypt/decrypt data using AES or RSA, and generate random values
  7. * @icon lock
  8. */
  9. const configSchema = {
  10. type: 'object',
  11. properties: {
  12. operation: {
  13. type: 'string',
  14. title: 'Operation',
  15. enum: ['hash', 'hmac', 'random', 'encrypt', 'decrypt'],
  16. enumLabels: ['Hash', 'HMAC', 'Random Generation', 'Encrypt', 'Decrypt'],
  17. default: 'hash',
  18. description: 'Cryptographic operation to perform'
  19. },
  20. algorithm: {
  21. type: 'string',
  22. title: 'Algorithm',
  23. enum: ['md5', 'sha1', 'sha256', 'sha384', 'sha512'],
  24. enumLabels: ['MD5', 'SHA-1', 'SHA-256', 'SHA-384', 'SHA-512'],
  25. default: 'sha256',
  26. description: 'Hash algorithm to use',
  27. showWhen: { field: 'operation', value: ['hash', 'hmac'] }
  28. },
  29. outputFormat: {
  30. type: 'string',
  31. title: 'Output Format',
  32. enum: ['hex', 'base64'],
  33. enumLabels: ['Hexadecimal', 'Base64'],
  34. default: 'hex',
  35. description: 'Format of the output hash or HMAC',
  36. showWhen: { field: 'operation', value: ['hash', 'hmac'] }
  37. },
  38. secretKey: {
  39. type: 'string',
  40. title: 'Secret Key',
  41. description: 'Secret key for HMAC generation. Supports {{variable}} interpolation.',
  42. showWhen: { field: 'operation', value: 'hmac' }
  43. },
  44. randomType: {
  45. type: 'string',
  46. title: 'Random Type',
  47. enum: ['uuid', 'hex', 'alphanumeric', 'password'],
  48. enumLabels: ['UUID v4', 'Hex Token', 'Alphanumeric String', 'Secure Password'],
  49. default: 'uuid',
  50. description: 'Type of random value to generate',
  51. showWhen: { field: 'operation', value: 'random' }
  52. },
  53. randomLength: {
  54. type: 'number',
  55. title: 'Length',
  56. default: 32,
  57. minimum: 1,
  58. maximum: 1024,
  59. description: 'Length of the generated value (1-1024). For hex, this is number of bytes (output will be 2x characters).',
  60. showWhen: { field: 'operation', value: 'random' }
  61. },
  62. passwordUppercase: {
  63. type: 'boolean',
  64. title: 'Include Uppercase',
  65. default: true,
  66. description: 'Include uppercase letters (A-Z)',
  67. showWhen: { field: 'randomType', value: 'password' }
  68. },
  69. passwordLowercase: {
  70. type: 'boolean',
  71. title: 'Include Lowercase',
  72. default: true,
  73. description: 'Include lowercase letters (a-z)',
  74. showWhen: { field: 'randomType', value: 'password' }
  75. },
  76. passwordNumbers: {
  77. type: 'boolean',
  78. title: 'Include Numbers',
  79. default: true,
  80. description: 'Include numbers (0-9)',
  81. showWhen: { field: 'randomType', value: 'password' }
  82. },
  83. passwordSymbols: {
  84. type: 'boolean',
  85. title: 'Include Symbols',
  86. default: true,
  87. description: 'Include symbols (!@#$%^&*()_+-=[]{}|;:,.<>?)',
  88. showWhen: { field: 'randomType', value: 'password' }
  89. },
  90. encryptionAlgorithm: {
  91. type: 'string',
  92. title: 'Encryption Algorithm',
  93. enum: ['aes-256-gcm', 'aes-256-cbc', 'rsa'],
  94. enumLabels: ['AES-256-GCM (Recommended)', 'AES-256-CBC', 'RSA'],
  95. default: 'aes-256-gcm',
  96. description: 'Encryption algorithm to use',
  97. showWhen: { field: 'operation', value: ['encrypt', 'decrypt'] }
  98. },
  99. encryptionKey: {
  100. type: 'string',
  101. title: 'Encryption Key',
  102. description: 'For AES: Base64-encoded 256-bit key OR passphrase (set Use Passphrase to true). For RSA encrypt: PEM public key. Supports {{variable}} interpolation.',
  103. showWhen: { field: 'operation', value: 'encrypt' }
  104. },
  105. decryptionKey: {
  106. type: 'string',
  107. title: 'Decryption Key',
  108. description: 'For AES: Base64-encoded 256-bit key OR passphrase (set Use Passphrase to true). For RSA decrypt: PEM private key. Supports {{variable}} interpolation.',
  109. showWhen: { field: 'operation', value: 'decrypt' }
  110. },
  111. usePassphrase: {
  112. type: 'boolean',
  113. title: 'Use Passphrase',
  114. default: false,
  115. description: 'If true, the key is treated as a passphrase and a 256-bit key is derived using PBKDF2',
  116. showWhen: { field: 'encryptionAlgorithm', value: ['aes-256-gcm', 'aes-256-cbc'] }
  117. },
  118. iv: {
  119. type: 'string',
  120. title: 'IV (Initialization Vector)',
  121. description: 'Optional. Base64-encoded IV. If not provided, a random IV will be generated for encryption. Required for decryption.',
  122. showWhen: { field: 'encryptionAlgorithm', value: ['aes-256-gcm', 'aes-256-cbc'] }
  123. },
  124. authTag: {
  125. type: 'string',
  126. title: 'Authentication Tag',
  127. description: 'Required for AES-GCM decryption. Base64-encoded authentication tag.',
  128. showWhen: { field: 'operation', value: 'decrypt' }
  129. },
  130. inputFormat: {
  131. type: 'string',
  132. title: 'Input Format',
  133. enum: ['string', 'base64'],
  134. enumLabels: ['String (UTF-8)', 'Base64'],
  135. default: 'string',
  136. description: 'Format of the input data for encryption, or ciphertext format for decryption',
  137. showWhen: { field: 'operation', value: ['encrypt', 'decrypt'] }
  138. },
  139. outputDataFormat: {
  140. type: 'string',
  141. title: 'Output Format',
  142. enum: ['base64', 'string'],
  143. enumLabels: ['Base64', 'String (UTF-8)'],
  144. default: 'base64',
  145. description: 'Format of the encrypted ciphertext output, or decrypted plaintext output',
  146. showWhen: { field: 'operation', value: ['encrypt', 'decrypt'] }
  147. }
  148. },
  149. required: ['operation']
  150. };
  151. const inputSchema = {
  152. type: 'object',
  153. properties: {
  154. data: {
  155. type: 'any',
  156. description: 'Data to process. For hash/hmac: string or will be JSON stringified. For encrypt: plaintext. For decrypt: ciphertext.'
  157. },
  158. secretKey: {
  159. type: 'string',
  160. description: 'Override secret key for HMAC from input'
  161. },
  162. encryptionKey: {
  163. type: 'string',
  164. description: 'Override encryption key from input (for encrypt operation)'
  165. },
  166. decryptionKey: {
  167. type: 'string',
  168. description: 'Override decryption key from input (for decrypt operation)'
  169. },
  170. iv: {
  171. type: 'string',
  172. description: 'Override IV from input (for AES decrypt)'
  173. },
  174. authTag: {
  175. type: 'string',
  176. description: 'Override auth tag from input (for AES-GCM decrypt)'
  177. },
  178. publicKey: {
  179. type: 'string',
  180. description: 'RSA public key in PEM format (for RSA encrypt)'
  181. },
  182. privateKey: {
  183. type: 'string',
  184. description: 'RSA private key in PEM format (for RSA decrypt)'
  185. }
  186. }
  187. };
  188. const outputSchema = {
  189. type: 'object',
  190. properties: {
  191. result: {
  192. type: 'string',
  193. description: 'The cryptographic operation result (hash, encrypted data, or decrypted data)'
  194. },
  195. algorithm: {
  196. type: 'string',
  197. description: 'Algorithm used for the operation'
  198. },
  199. operation: {
  200. type: 'string',
  201. description: 'Operation that was performed'
  202. },
  203. format: {
  204. type: 'string',
  205. description: 'Output format (hex or base64)'
  206. },
  207. randomType: {
  208. type: 'string',
  209. description: 'Type of random value generated (uuid, hex, alphanumeric, password)'
  210. },
  211. length: {
  212. type: 'number',
  213. description: 'Length of the generated random value'
  214. },
  215. inputLength: {
  216. type: 'number',
  217. description: 'Length of the input data in bytes'
  218. },
  219. iv: {
  220. type: 'string',
  221. description: 'Base64-encoded IV used for encryption (save this for decryption)'
  222. },
  223. authTag: {
  224. type: 'string',
  225. description: 'Base64-encoded authentication tag for AES-GCM (save this for decryption)'
  226. }
  227. }
  228. };
  229. function interpolate(template, data) {
  230. if (typeof template !== 'string') return template;
  231. return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
  232. const keys = key.trim().split('.');
  233. let value = data;
  234. for (const k of keys) {
  235. if (value && typeof value === 'object' && k in value) {
  236. value = value[k];
  237. } else {
  238. return match;
  239. }
  240. }
  241. return value !== undefined ? String(value) : match;
  242. });
  243. }
  244. function generateRandomString(length, charset) {
  245. const charsetLength = charset.length;
  246. let result = '';
  247. const bytesNeeded = length;
  248. const randomHex = smartbotic.crypto.randomBytes(bytesNeeded * 2);
  249. let pos = 0;
  250. while (result.length < length) {
  251. if (pos >= randomHex.length) {
  252. const moreBytes = smartbotic.crypto.randomBytes((length - result.length) * 2);
  253. pos = 0;
  254. for (let i = 0; i < moreBytes.length && result.length < length; i += 2) {
  255. const byte = parseInt(moreBytes.substr(i, 2), 16);
  256. const maxUnbiased = Math.floor(256 / charsetLength) * charsetLength;
  257. if (byte < maxUnbiased) {
  258. result += charset[byte % charsetLength];
  259. }
  260. }
  261. } else {
  262. const byte = parseInt(randomHex.substr(pos, 2), 16);
  263. pos += 2;
  264. const maxUnbiased = Math.floor(256 / charsetLength) * charsetLength;
  265. if (byte < maxUnbiased) {
  266. result += charset[byte % charsetLength];
  267. }
  268. }
  269. }
  270. return result;
  271. }
  272. async function execute(config, input, context) {
  273. const operation = config.operation || 'hash';
  274. if (operation === 'encrypt') {
  275. return executeEncrypt(config, input);
  276. }
  277. if (operation === 'decrypt') {
  278. return executeDecrypt(config, input);
  279. }
  280. if (operation === 'random') {
  281. const randomType = config.randomType || 'uuid';
  282. const length = config.randomLength || 32;
  283. if (randomType !== 'uuid' && (length < 1 || length > 1024)) {
  284. throw new Error('Length must be between 1 and 1024');
  285. }
  286. if (randomType === 'uuid') {
  287. const uuid = smartbotic.crypto.randomUUID();
  288. smartbotic.log.info('Generated random UUID v4');
  289. return {
  290. result: uuid,
  291. operation: 'random',
  292. randomType: 'uuid',
  293. length: 36
  294. };
  295. }
  296. if (randomType === 'hex') {
  297. const randomHex = smartbotic.crypto.randomBytes(length);
  298. smartbotic.log.info('Generated ' + length + '-byte hex token (' + randomHex.length + ' characters)');
  299. return {
  300. result: randomHex,
  301. operation: 'random',
  302. randomType: 'hex',
  303. length: randomHex.length
  304. };
  305. }
  306. if (randomType === 'alphanumeric') {
  307. const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  308. const result = generateRandomString(length, charset);
  309. smartbotic.log.info('Generated ' + length + '-character alphanumeric string');
  310. return {
  311. result: result,
  312. operation: 'random',
  313. randomType: 'alphanumeric',
  314. length: length
  315. };
  316. }
  317. if (randomType === 'password') {
  318. const includeUppercase = config.passwordUppercase !== false;
  319. const includeLowercase = config.passwordLowercase !== false;
  320. const includeNumbers = config.passwordNumbers !== false;
  321. const includeSymbols = config.passwordSymbols !== false;
  322. let charset = '';
  323. if (includeUppercase) charset += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
  324. if (includeLowercase) charset += 'abcdefghijklmnopqrstuvwxyz';
  325. if (includeNumbers) charset += '0123456789';
  326. if (includeSymbols) charset += '!@#$%^&*()_+-=[]{}|;:,.<>?';
  327. if (charset.length === 0) {
  328. throw new Error('At least one character set must be enabled for password generation');
  329. }
  330. const result = generateRandomString(length, charset);
  331. smartbotic.log.info('Generated ' + length + '-character secure password');
  332. return {
  333. result: result,
  334. operation: 'random',
  335. randomType: 'password',
  336. length: length
  337. };
  338. }
  339. throw new Error('Unknown random type: ' + randomType);
  340. }
  341. const inputData = input.data !== undefined ? input.data : input;
  342. let dataStr;
  343. if (typeof inputData === 'string') {
  344. dataStr = inputData;
  345. } else if (inputData === null || inputData === undefined) {
  346. throw new Error('No data provided for ' + operation + ' operation');
  347. } else {
  348. dataStr = JSON.stringify(inputData);
  349. }
  350. const algorithm = config.algorithm || 'sha256';
  351. const outputFormat = config.outputFormat || 'hex';
  352. if (operation === 'hash') {
  353. smartbotic.log.info('Hashing ' + dataStr.length + ' bytes with ' + algorithm.toUpperCase());
  354. const result = smartbotic.crypto.hash(dataStr, algorithm, outputFormat);
  355. return {
  356. result: result,
  357. algorithm: algorithm,
  358. operation: 'hash',
  359. format: outputFormat,
  360. inputLength: dataStr.length
  361. };
  362. }
  363. if (operation === 'hmac') {
  364. let secretKey = input.secretKey || config.secretKey;
  365. if (secretKey && typeof secretKey === 'string') {
  366. secretKey = interpolate(secretKey, input);
  367. }
  368. if (!secretKey) {
  369. throw new Error('Secret key is required for HMAC operation');
  370. }
  371. smartbotic.log.info('Generating HMAC-' + algorithm.toUpperCase() + ' for ' + dataStr.length + ' bytes');
  372. const result = smartbotic.crypto.hmac(dataStr, secretKey, algorithm, outputFormat);
  373. return {
  374. result: result,
  375. algorithm: algorithm,
  376. operation: 'hmac',
  377. format: outputFormat,
  378. inputLength: dataStr.length
  379. };
  380. }
  381. throw new Error('Unknown operation: ' + operation);
  382. }
  383. function executeEncrypt(config, input) {
  384. const encryptionAlgorithm = config.encryptionAlgorithm || 'aes-256-gcm';
  385. const inputFormat = config.inputFormat || 'string';
  386. let inputData = input.data !== undefined ? input.data : input;
  387. if (typeof inputData !== 'string') {
  388. if (inputData === null || inputData === undefined) {
  389. throw new Error('No data provided for encryption');
  390. }
  391. inputData = JSON.stringify(inputData);
  392. }
  393. let dataToEncrypt = inputData;
  394. if (inputFormat === 'base64') {
  395. dataToEncrypt = smartbotic.utils.base64Decode(inputData);
  396. if (!dataToEncrypt) {
  397. throw new Error('Invalid base64 input data');
  398. }
  399. }
  400. if (encryptionAlgorithm === 'rsa') {
  401. return executeRsaEncrypt(config, input, dataToEncrypt);
  402. }
  403. return executeAesEncrypt(config, input, dataToEncrypt, encryptionAlgorithm);
  404. }
  405. function executeAesEncrypt(config, input, dataToEncrypt, encryptionAlgorithm) {
  406. let key = input.encryptionKey || config.encryptionKey;
  407. if (key && typeof key === 'string') {
  408. key = interpolate(key, input);
  409. }
  410. if (!key) {
  411. throw new Error('Encryption key is required for AES encryption');
  412. }
  413. let iv = input.iv || config.iv;
  414. if (iv && typeof iv === 'string') {
  415. iv = interpolate(iv, input);
  416. }
  417. const usePassphrase = config.usePassphrase === true;
  418. const algorithm = encryptionAlgorithm === 'aes-256-cbc' ? 'cbc' : 'gcm';
  419. smartbotic.log.info('Encrypting ' + dataToEncrypt.length + ' bytes with AES-256-' + algorithm.toUpperCase());
  420. const result = smartbotic.crypto.aesEncrypt({
  421. data: dataToEncrypt,
  422. key: key,
  423. iv: iv || '',
  424. algorithm: algorithm,
  425. keyIsPassphrase: usePassphrase
  426. });
  427. if (result.error) {
  428. throw new Error('Encryption failed: ' + result.error);
  429. }
  430. const outputResult = {
  431. result: result.ciphertext,
  432. operation: 'encrypt',
  433. algorithm: encryptionAlgorithm,
  434. iv: result.iv,
  435. inputLength: dataToEncrypt.length
  436. };
  437. if (algorithm === 'gcm') {
  438. outputResult.authTag = result.authTag;
  439. }
  440. return outputResult;
  441. }
  442. function executeRsaEncrypt(config, input, dataToEncrypt) {
  443. let publicKey = input.publicKey || input.encryptionKey || config.encryptionKey;
  444. if (publicKey && typeof publicKey === 'string') {
  445. publicKey = interpolate(publicKey, input);
  446. }
  447. if (!publicKey) {
  448. throw new Error('Public key is required for RSA encryption');
  449. }
  450. if (!publicKey.includes('-----BEGIN')) {
  451. throw new Error('Public key must be in PEM format (starting with -----BEGIN PUBLIC KEY-----)');
  452. }
  453. smartbotic.log.info('Encrypting ' + dataToEncrypt.length + ' bytes with RSA');
  454. const result = smartbotic.crypto.rsaEncrypt({
  455. data: dataToEncrypt,
  456. publicKey: publicKey
  457. });
  458. if (result.error) {
  459. throw new Error('RSA encryption failed: ' + result.error);
  460. }
  461. return {
  462. result: result.ciphertext,
  463. operation: 'encrypt',
  464. algorithm: 'rsa',
  465. inputLength: dataToEncrypt.length
  466. };
  467. }
  468. function executeDecrypt(config, input) {
  469. const encryptionAlgorithm = config.encryptionAlgorithm || 'aes-256-gcm';
  470. const outputDataFormat = config.outputDataFormat || 'base64';
  471. let inputData = input.data !== undefined ? input.data : input;
  472. if (typeof inputData !== 'string') {
  473. if (inputData === null || inputData === undefined) {
  474. throw new Error('No data provided for decryption');
  475. }
  476. throw new Error('Ciphertext must be a base64-encoded string');
  477. }
  478. if (encryptionAlgorithm === 'rsa') {
  479. return executeRsaDecrypt(config, input, inputData, outputDataFormat);
  480. }
  481. return executeAesDecrypt(config, input, inputData, encryptionAlgorithm, outputDataFormat);
  482. }
  483. function executeAesDecrypt(config, input, ciphertext, encryptionAlgorithm, outputDataFormat) {
  484. let key = input.decryptionKey || config.decryptionKey;
  485. if (key && typeof key === 'string') {
  486. key = interpolate(key, input);
  487. }
  488. if (!key) {
  489. throw new Error('Decryption key is required for AES decryption');
  490. }
  491. let iv = input.iv || config.iv;
  492. if (iv && typeof iv === 'string') {
  493. iv = interpolate(iv, input);
  494. }
  495. if (!iv) {
  496. throw new Error('IV is required for AES decryption');
  497. }
  498. const usePassphrase = config.usePassphrase === true;
  499. const algorithm = encryptionAlgorithm === 'aes-256-cbc' ? 'cbc' : 'gcm';
  500. const decryptOptions = {
  501. ciphertext: ciphertext,
  502. key: key,
  503. iv: iv,
  504. algorithm: algorithm,
  505. keyIsPassphrase: usePassphrase
  506. };
  507. if (algorithm === 'gcm') {
  508. let authTag = input.authTag || config.authTag;
  509. if (authTag && typeof authTag === 'string') {
  510. authTag = interpolate(authTag, input);
  511. }
  512. if (!authTag) {
  513. throw new Error('Authentication tag is required for AES-GCM decryption');
  514. }
  515. decryptOptions.authTag = authTag;
  516. }
  517. smartbotic.log.info('Decrypting with AES-256-' + algorithm.toUpperCase());
  518. const result = smartbotic.crypto.aesDecrypt(decryptOptions);
  519. if (result.error) {
  520. throw new Error('Decryption failed: ' + result.error);
  521. }
  522. let outputResult = result.plaintext;
  523. if (outputDataFormat === 'base64') {
  524. outputResult = smartbotic.utils.base64Encode(result.plaintext);
  525. }
  526. return {
  527. result: outputResult,
  528. operation: 'decrypt',
  529. algorithm: encryptionAlgorithm
  530. };
  531. }
  532. function executeRsaDecrypt(config, input, ciphertext, outputDataFormat) {
  533. let privateKey = input.privateKey || input.decryptionKey || config.decryptionKey;
  534. if (privateKey && typeof privateKey === 'string') {
  535. privateKey = interpolate(privateKey, input);
  536. }
  537. if (!privateKey) {
  538. throw new Error('Private key is required for RSA decryption');
  539. }
  540. if (!privateKey.includes('-----BEGIN')) {
  541. throw new Error('Private key must be in PEM format (starting with -----BEGIN PRIVATE KEY----- or -----BEGIN RSA PRIVATE KEY-----)');
  542. }
  543. smartbotic.log.info('Decrypting with RSA');
  544. const result = smartbotic.crypto.rsaDecrypt({
  545. ciphertext: ciphertext,
  546. privateKey: privateKey
  547. });
  548. if (result.error) {
  549. throw new Error('RSA decryption failed: ' + result.error);
  550. }
  551. let outputResult = result.plaintext;
  552. if (outputDataFormat === 'base64') {
  553. outputResult = smartbotic.utils.base64Encode(result.plaintext);
  554. }
  555. return {
  556. result: outputResult,
  557. operation: 'decrypt',
  558. algorithm: 'rsa'
  559. };
  560. }
  561. module.exports = { configSchema, inputSchema, outputSchema, execute };