diff --git a/package-lock.json b/package-lock.json index 301b682d..55d68361 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1420,6 +1420,10 @@ "resolved": "recipes/create-require-from-path", "link": true }, + "node_modules/@nodejs/crypto-rsa-pss-update": { + "resolved": "recipes/crypto-rsa-pss-update", + "link": true + }, "node_modules/@nodejs/fs-access-mode-constants": { "resolved": "recipes/fs-access-mode-constants", "link": true @@ -4195,6 +4199,17 @@ "@codemod.com/jssg-types": "^1.0.9" } }, + "recipes/crypto-rsa-pss-update": { + "name": "@nodejs/crypto-rsa-pss-update", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.3" + } + }, "recipes/fs-access-mode-constants": { "name": "@nodejs/fs-access-mode-constants", "version": "1.0.0", diff --git a/recipes/crypto-rsa-pss-update/README.md b/recipes/crypto-rsa-pss-update/README.md new file mode 100644 index 00000000..fd998952 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/README.md @@ -0,0 +1,78 @@ +# crypto-rsa-pss-update + +Codemod to handle Node.js crypto deprecation DEP0154 by transforming deprecated RSA-PSS key generation options. + +## What it does + +This codemod transforms deprecated RSA-PSS crypto options in `crypto.generateKeyPair()` and `crypto.generateKeyPairSync()` calls: + +- `hash` → `hashAlgorithm` +- `mgf1Hash` → `mgf1HashAlgorithm` + +The transformation only applies to calls with `'rsa-pss'` as the key type. + +## Examples + +**Before** + +```js +const crypto = require("node:crypto"); + +crypto.generateKeyPair( + "rsa-pss", + { + modulusLength: 2048, + hash: "sha256", + mgf1Hash: "sha1", + saltLength: 32, + }, + (err, publicKey, privateKey) => { + // callback + }, +); + +crypto.generateKeyPairSync("rsa-pss", { + modulusLength: 2048, + hash: "sha256", +}); +``` + +**After** + +```js +const crypto = require("crypto"); + +crypto.generateKeyPair( + "rsa-pss", + { + modulusLength: 2048, + hashAlgorithm: "sha256", + mgf1HashAlgorithm: "sha1", + saltLength: 32, + }, + (err, publicKey, privateKey) => { + // callback + }, +); + +crypto.generateKeyPairSync("rsa-pss", { + modulusLength: 2048, + hashAlgorithm: "sha256", +}); +``` + +## Usage + +```bash +npx codemod @nodejs/crypto-rsa-pss-update +``` + +## Supports + +- Both `crypto.generateKeyPair()` and `crypto.generateKeyPairSync()` +- Destructured imports: `const { generateKeyPair } = require('crypto')` +- Variable references: `const options = { hash: 'sha256' }` +- Function calls: `getKeyOptions()` returning crypto options +- This property patterns: `this.options = { hash: 'sha256' }` +- Only transforms `'rsa-pss'` key type calls +- Preserves all other options and call structure diff --git a/recipes/crypto-rsa-pss-update/codemod.yaml b/recipes/crypto-rsa-pss-update/codemod.yaml new file mode 100644 index 00000000..1d72aeda --- /dev/null +++ b/recipes/crypto-rsa-pss-update/codemod.yaml @@ -0,0 +1,25 @@ +schema_version: "1.0" +name: "@nodejs/crypto-rsa-pss-update" +version: 1.0.0 +description: >- + Handle DEP0154 via transforming deprecated RSA-PSS crypto options `hash` to + `hashAlgorithm` and `mgf1Hash` to `mgf1HashAlgorithm` +author: Mauro Nievas +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - crypto + - rsa-pss + +registry: + access: public + visibility: public diff --git a/recipes/crypto-rsa-pss-update/package.json b/recipes/crypto-rsa-pss-update/package.json new file mode 100644 index 00000000..3eb88a68 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/crypto-rsa-pss-update", + "version": "1.0.0", + "description": "Handle DEP0154 via transforming deprecated RSA-PSS crypto options `hash` to `hashAlgorithm` and `mgf1Hash` to `mgf1HashAlgorithm`.", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/crypto-rsa-pss-update", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Mauro Nievas", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/crypto-rsa-pss-update/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.3" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/src/workflow.ts b/recipes/crypto-rsa-pss-update/src/workflow.ts new file mode 100644 index 00000000..bb746be4 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/src/workflow.ts @@ -0,0 +1,317 @@ +import type { SgRoot, SgNode, Edit, TypesMap, Kinds } from "@codemod.com/jssg-types/main"; +import type JS from "@codemod.com/jssg-types/langs/javascript"; +import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement"; +import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call"; +import { resolveBindingPath } from "@nodejs/codemod-utils/ast-grep/resolve-binding-path"; + +const RSA_PSS_REGEX = /^['"]rsa-pss['"]$/; +const IDENTIFIER_REGEX = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; +const HASH_MAPPINGS = { + hash: "hashAlgorithm", + mgf1Hash: "mgf1HashAlgorithm" +} as const; + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const cryptoBindings = getCryptoBindings(root); + const allCalls = findCryptoCalls(rootNode, cryptoBindings); + + const allEdits = [ + ...transformRsaPssCalls(rootNode, allCalls), + ...transformSpreadObjectDeclarations(rootNode, allCalls), + ...processThisPropertyReferences(rootNode, allCalls), + ...transformPropertyAssignments(rootNode), + ...transformVariableHashStrings(rootNode) + ]; + + return allEdits.length ? rootNode.commitEdits(allEdits) : null; +} + + +function transformRsaPssCalls(rootNode: SgNode, allCalls: SgNode[]): Edit[] { + const edits: Edit[] = []; + + for (const call of allCalls) { + const typeMatch = call.getMatch("TYPE"); + const optionsMatch = call.getMatch("OPTIONS"); + + if (!typeMatch || !optionsMatch) continue; + + const typeText = getText(typeMatch); + if (!typeText) continue; + + if (!isRsaPssType(rootNode, typeText)) continue; + + + const directObject = optionsMatch.find({ + rule: { + kind: "object" + } + }); + + if (directObject) { + edits.push(...transformHashPropertiesInObject(directObject)); + } else { + edits.push(...processOptionsReference(rootNode, optionsMatch)); + } + } + + return edits; +} + +function getCryptoBindings(root: SgRoot): string[] { + const bindings = resolveBindings(getModuleStatements(root, "crypto"), ["$.generateKeyPair", "$.generateKeyPairSync"]); + return [...bindings, ...getPromisifiedBindings(root, bindings)]; +} + +function getPromisifiedBindings(root: SgRoot, existingBindings: string[]): string[] { + const utilStatements = getModuleStatements(root, "util"); + const promisifyBindings = resolveBindings(utilStatements, "$.promisify"); + + if (promisifyBindings.length === 0 && utilStatements.length > 0) { + promisifyBindings.push("util.promisify"); + } + + return uniqueArray( + existingBindings.flatMap(binding => + promisifyBindings.flatMap(promisifyBinding => + findPromisifiedDeclarations(root.root(), binding, promisifyBinding) + ) + ) + ); +} + +function findCryptoCalls(rootNode: SgNode, bindings: string[]): SgNode[] { + return bindings + .flatMap(bindingName => rootNode.findAll({ + rule: { + any: [ + { pattern: `${bindingName}($TYPE, $OPTIONS, $CALLBACK)` }, + { pattern: `${bindingName}($TYPE, $OPTIONS)` } + ] + } + })); +} + +function getText(node: SgNode | undefined): string | null { + const text = node?.text()?.trim(); + return text || null; +} + +function getModuleStatements(root: SgRoot, moduleName: string): SgNode[] { + const importStatements = getNodeImportStatements(root, moduleName); + const requireCalls = getNodeRequireCalls(root, moduleName); + return [...importStatements, ...requireCalls]; +} + + +function resolveBindings(statements: SgNode[], paths: string | string[]): string[] { + const pathArray = Array.isArray(paths) ? paths : [paths]; + + return statements.flatMap(stmt => + pathArray + .map(path => resolveBindingPath(stmt as unknown as SgNode, path)) + .filter(Boolean) + ); +} + + +function isValidHashKey(key: string | undefined): key is "hash" | "mgf1Hash" { + return key === "hash" || key === "mgf1Hash"; +} + +function findPromisifiedDeclarations(rootNode: SgNode, binding: string, promisifyBinding: string): string[] { + const promisified = rootNode.findAll({ + rule: { + kind: "lexical_declaration", + has: { + kind: "variable_declarator", + has: { + kind: "call_expression", + pattern: `${promisifyBinding}(${binding})` + } + } + } + }); + + return promisified + .map(decl => { + const variableDeclarator = decl.find({ rule: { kind: "variable_declarator" }}); + const identifier = variableDeclarator?.child(0); + return identifier?.kind() === "identifier" ? getText(identifier) : null; + }) + .filter(Boolean); +} + +function checkVariableDeclarations(rootNode: SgNode, declarationType: "const" | "let", identifier: string, expectedValue: RegExp): boolean { + const declarations = rootNode.findAll({ + rule: { + pattern: `${declarationType} ${identifier} = $VALUE` + } + }); + + return declarations.some(decl => { + const valueText = getText(decl.getMatch("VALUE")); + return valueText && expectedValue.test(valueText); + }); +} + +function isRsaPssType(rootNode: SgNode, typeText: string): boolean { + return RSA_PSS_REGEX.test(typeText) || + (IDENTIFIER_REGEX.test(typeText) && + (checkVariableDeclarations(rootNode, "const", typeText, RSA_PSS_REGEX) || + checkVariableDeclarations(rootNode, "let", typeText, RSA_PSS_REGEX))); +} + +function transformHashPropertiesInObject(objectNode: SgNode): Edit[] { + return objectNode.findAll({ rule: { kind: "pair" }}) + .map(pair => { + const keyNode = pair.find({ + rule: { + any: [ + { regex: "hash", kind: "property_identifier" }, + { regex: "mgf1Hash", kind: "property_identifier" } + ] + } + }); + + const key = getText(keyNode); + if (!isValidHashKey(key)) return null; + + const valueNode = pair.find({ + rule: { + any: [ + { kind: "string" }, + { kind: "identifier" }, + { kind: "template_string" }, + { kind: "member_expression" }, + { kind: "call_expression" }, + { kind: "binary_expression" }, + { kind: "ternary_expression" }, + { kind: "spread_element" } + ] + } + }); + const value = getText(valueNode); + if (!value) return null; + + return pair.replace(`${HASH_MAPPINGS[key]}: ${value}`); + }) + .filter(Boolean); +} + + +function processOptionsReference(rootNode: SgNode, optionsMatch: SgNode): Edit[] { + const optionsText = getText(optionsMatch); + if (!optionsText) return []; + + if (IDENTIFIER_REGEX.test(optionsText)) { + return findAndTransformObjects(rootNode, [`const ${optionsText} = { $$$PROPS }`]); + } + + if (optionsMatch.find({ rule: { kind: "call_expression" }})) { + const functionName = optionsText.replace(/\(\).*$/, ''); + return findAndTransformObjects(rootNode, [ + `function ${functionName}() { return { $$$PROPS } }`, + `const ${functionName} = () => ({ $$$PROPS })`, + `const ${functionName} = function() { return { $$$PROPS } }` + ]); + } + + return []; +} + +function getOptionsMatches(allCalls: SgNode[]): SgNode[] { + return allCalls.map(call => call.getMatch("OPTIONS")).filter(Boolean); +} + +function uniqueArray(items: T[]): T[] { + return Array.from(new Set(items)); +} + +function findAndTransformObjects(rootNode: SgNode, patterns: string[]): Edit[] { + return patterns.flatMap(pattern => + rootNode.findAll({ rule: { pattern }}).flatMap(decl => transformHashPropertiesInObject(decl)) + ); +} + +function transformSpreadObjectDeclarations(rootNode: SgNode, allCalls: SgNode[]): Edit[] { + const spreadNames = uniqueArray( + getOptionsMatches(allCalls) + .flatMap(optionsMatch => + optionsMatch.findAll({ rule: { kind: "spread_element" }}) + .map(spread => getText(spread.find({ rule: { kind: "identifier" }}))) + .filter(Boolean) + ) + ); + + const patterns = spreadNames.map(spreadName => `const ${spreadName} = { $$$PROPS }`); + return findAndTransformObjects(rootNode, patterns); +} + +function processThisPropertyReferences(rootNode: SgNode, allCalls: SgNode[]): Edit[] { + const propertyNames = uniqueArray( + getOptionsMatches(allCalls) + .map(optionsMatch => getText(optionsMatch)) + .filter(text => text?.startsWith('this.')) + .map(text => text!.replace('this.', '')) + ); + + const patterns = propertyNames.map(propName => `this.${propName} = { $$$PROPS }`); + return findAndTransformObjects(rootNode, patterns); +} + +function transformAssignmentPattern(rootNode: SgNode, oldProperty: string, newProperty: string): Edit[] { + const assignments = rootNode.findAll({ + rule: { + pattern: `$OBJECT.${oldProperty} = $VALUE` + } + }); + + return assignments + .map(assignment => { + const objectMatch = assignment.getMatch("OBJECT"); + const valueMatch = assignment.getMatch("VALUE"); + + if (objectMatch && valueMatch) { + const objectText = getText(objectMatch); + const valueText = getText(valueMatch); + + if (objectText && valueText) { + return assignment.replace(`${objectText}.${newProperty} = ${valueText}`); + } + } + return null; + }) + .filter(Boolean); +} + +function transformPropertyAssignments(rootNode: SgNode): Edit[] { + return [ + ...transformAssignmentPattern(rootNode, "hash", "hashAlgorithm"), + ...transformAssignmentPattern(rootNode, "mgf1Hash", "mgf1HashAlgorithm") + ]; +} + +function transformVariableHashStrings(rootNode: SgNode): Edit[] { + return findAndTransformVariableDeclarations(rootNode, [ + { from: "'hash'", to: "'hashAlgorithm'" }, + { from: "'mgf1Hash'", to: "'mgf1HashAlgorithm'" }, + { from: "'mgf1' + 'Hash'", to: "'mgf1HashAlgorithm'" }, + { from: "'mgf1' + 'HashAlgorithm'", to: "'mgf1HashAlgorithm'" } + ]); +} + +function findAndTransformVariableDeclarations(rootNode: SgNode, transformations: Array<{from: string, to: string}>): Edit[] { + return transformations.flatMap(({ from, to }) => + rootNode.findAll({ + rule: { + any: [ + { pattern: `const $VAR = ${from}` }, + { pattern: `let $VAR = ${from}` }, + { pattern: `var $VAR = ${from}` } + ] + } + }).map(decl => decl.replace(decl.text().replace(from, to))) + ); +} \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/basic.js b/recipes/crypto-rsa-pss-update/tests/expected/basic.js new file mode 100644 index 00000000..f310f8c8 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/basic.js @@ -0,0 +1,17 @@ +const crypto = require('crypto'); + +// Basic case with hash option only +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha256', + saltLength: 32 +}, (err, publicKey, privateKey) => { + console.log('Generated keys'); +}); + +// Both hash and mgf1Hash options together +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha1' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/complex-ast-patterns.js b/recipes/crypto-rsa-pss-update/tests/expected/complex-ast-patterns.js new file mode 100644 index 00000000..8c2067c2 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/complex-ast-patterns.js @@ -0,0 +1,53 @@ +const crypto = require('crypto'); + +// Test case 1: member_expression - accessing constants +const constants = { + SECURE_HASH: 'sha512', + MGF_HASH: 'sha256' +}; + +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: constants.SECURE_HASH, // ← member_expression + saltLength: 32 +}); + +// Test case 2: call_expression - functions returning algorithms +function getSecureHash() { + return 'sha256'; +} + +function getMgfHash() { + return 'sha1'; +} + +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: getSecureHash(), // ← call_expression + mgf1HashAlgorithm: getMgfHash() // ← call_expression +}); + +// Test case 3: binary_expression - string concatenation +const bitLength = '256'; +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha' + bitLength, // ← binary_expression + mgf1HashAlgorithm: 'sha' + '1' // ← binary_expression +}); + +// Test case 4: conditional_expression - ternary operator +const isProduction = true; +const useStrongSecurity = false; + +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: isProduction ? 'sha512' : 'sha256', // ← conditional_expression + mgf1HashAlgorithm: useStrongSecurity ? 'sha256' : 'sha1' // ← conditional_expression +}); + +// Test case 5: Complex mixed cases +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: process.env.NODE_ENV === 'production' ? crypto.constants.defaultHash || 'sha512' : 'sha256', // ← multiple types + saltLength: 32 +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/computed-properties.js b/recipes/crypto-rsa-pss-update/tests/expected/computed-properties.js new file mode 100644 index 00000000..1c7c8117 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/computed-properties.js @@ -0,0 +1,45 @@ +const crypto = require('crypto'); + +// Computed property names - ideally should be transformed +const hashKey = 'hashAlgorithm'; +const mgfKey = 'mgf1HashAlgorithm'; + +// Case 1: Computed property with bracket notation +const options1 = { + modulusLength: 2048, + [hashKey]: 'sha256', // Ideally should transform to hashAlgorithm + mgf1HashAlgorithm: 'sha1' // Should transform (static) +}; + +crypto.generateKeyPair('rsa-pss', options1, (err, publicKey, privateKey) => { + console.log('Generated with computed hash property'); +}); + +// Case 2: Both properties computed +const options2 = { + [hashKey]: 'sha256', // Ideally should transform + [mgfKey]: 'sha1' // Ideally should transform +}; + +crypto.generateKeyPairSync('rsa-pss', options2); + +// Case 3: Mixed static and computed +const options3 = { + hashAlgorithm: 'sha256', // Should transform + [mgfKey]: 'sha1', // Ideally should transform + modulusLength: 2048 +}; + +crypto.generateKeyPair('rsa-pss', options3); + +// Case 4: Dynamic key construction +const propertyName = 'hashAlgorithm'; +const anotherProperty = 'mgf1HashAlgorithm'; + +const options4 = { + [propertyName]: 'sha256', + [anotherProperty]: 'sha1', + modulusLength: 2048 +}; + +crypto.generateKeyPairSync('rsa-pss', options4); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/destructured.js b/recipes/crypto-rsa-pss-update/tests/expected/destructured.js new file mode 100644 index 00000000..89b1e9e1 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/destructured.js @@ -0,0 +1,67 @@ +// Comprehensive import patterns test +import { generateKeyPair } from 'crypto'; +import crypto as nodeCrypto from 'crypto'; +import { generateKeyPair as keyGen } from 'node:crypto'; +const { generateKeyPairSync } = require('node:crypto'); +const { generateKeyPair: aliasedGenerateKeyPair, generateKeyPairSync: foo } = require('crypto'); +const cryptoLib = require('crypto'); + +// ES6 import destructuring +generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha256' +}, (err, publicKey, privateKey) => { + console.log('ES6 import'); +}); + +// CommonJS destructuring +generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + mgf1HashAlgorithm: 'sha1' +}); + +// Aliased destructuring with meaningful names +aliasedGenerateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha512', + mgf1HashAlgorithm: 'sha256' +}); + +// Aliased destructuring with short names +foo('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha384' +}); + +// Namespace import with alias +nodeCrypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha256', + saltLength: 32 +}, (err, publicKey, privateKey) => { + console.log('Generated keys with aliased import'); +}); + +// Function alias from destructuring +keyGen('rsa-pss', { + modulusLength: 2048, + mgf1HashAlgorithm: 'sha1' +}); + +// Variable assignment +cryptoLib.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha512' +}); + +// Mixed CJS + ESM imports (consolidated from mixed-imports.js) +import { constants, randomBytes } from 'crypto'; + +// Using constants from ESM import +const optionsMixed = { + hashAlgorithm: constants.defaultCipherName || 'sha256', + mgf1HashAlgorithm: 'sha1', + modulusLength: 2048 +}; + +cryptoLib.generateKeyPair('rsa-pss', optionsMixed); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/dynamic-options.js b/recipes/crypto-rsa-pss-update/tests/expected/dynamic-options.js new file mode 100644 index 00000000..ed63072f --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/dynamic-options.js @@ -0,0 +1,22 @@ +const crypto = require('crypto'); + +// Dynamic options object - now supported by codemod +const options = { + modulusLength: 2048, + hashAlgorithm: 'sha256', + saltLength: 32 +}; + +crypto.generateKeyPair('rsa-pss', options, (err, publicKey, privateKey) => { + console.log('Generated keys with dynamic options'); +}); + +// Function returning options +function getKeyOptions() { + return { + modulusLength: 2048, + mgf1HashAlgorithm: 'sha1' + }; +} + +crypto.generateKeyPairSync('rsa-pss', getKeyOptions()); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/method-chaining.js b/recipes/crypto-rsa-pss-update/tests/expected/method-chaining.js new file mode 100644 index 00000000..db23c0d0 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/method-chaining.js @@ -0,0 +1,63 @@ +const crypto = require('crypto'); +const { promisify } = require('util'); + +// Case 1: Promise chaining with crypto.generateKeyPair +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha1' +}, (err, publicKey, privateKey) => { + console.log('Generated keys'); +}); + +// Case 2: Method chaining with promisified version +const generateKeyPairAsync = promisify(crypto.generateKeyPair); + +generateKeyPairAsync('rsa-pss', { + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha1', + modulusLength: 2048 +}).then(({ publicKey, privateKey }) => { + console.log('Keys generated via promise'); +}).catch(console.error); + +// Case 3: Async/await with inline options +async function generateKeys() { + try { + const { publicKey, privateKey } = await generateKeyPairAsync('rsa-pss', { + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha1' + }); + return { publicKey, privateKey }; + } catch (error) { + console.error('Key generation failed:', error); + } +} + +// Case 4: Class method with fluent API pattern +class KeyGenerator { + constructor() { + this.options = {}; + } + + setHash(algorithm) { + this.options.hashAlgorithm = algorithm; + return this; + } + + setMgf1Hash(algorithm) { + this.options.mgf1HashAlgorithm = algorithm; + return this; + } + + async generate() { + return generateKeyPairAsync('rsa-pss', this.options); + } +} + +// Usage +new KeyGenerator() + .setHash('sha256') + .setMgf1Hash('sha1') + .generate() + .then(console.log); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/namespace-import.mjs b/recipes/crypto-rsa-pss-update/tests/expected/namespace-import.mjs new file mode 100644 index 00000000..8bc3fcad --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/namespace-import.mjs @@ -0,0 +1,16 @@ +import * as crypto from 'node:crypto'; + +// This should be transformed but currently fails +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha256', + saltLength: 32 +}, (err, publicKey, privateKey) => { + console.log('Generated keys'); +}); + +// This should also be transformed +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + mgf1HashAlgorithm: 'sha256' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/nested-objects.js b/recipes/crypto-rsa-pss-update/tests/expected/nested-objects.js new file mode 100644 index 00000000..8c791311 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/nested-objects.js @@ -0,0 +1,17 @@ +const crypto = require('crypto'); + +// Nested object structure test +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha256', + saltLength: 32 +}, (err, publicKey, privateKey) => { + console.log('Generated keys'); +}); + +// Mixed nested and direct properties +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha1' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/non-rsa-pss.js b/recipes/crypto-rsa-pss-update/tests/expected/non-rsa-pss.js new file mode 100644 index 00000000..2a0b014f --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/non-rsa-pss.js @@ -0,0 +1,20 @@ +const crypto = require('crypto'); + +// Should NOT be transformed - different key type +crypto.generateKeyPair('rsa', { + modulusLength: 2048, + hash: 'sha256' +}, (err, publicKey, privateKey) => { + // callback +}); + +// Should NOT be transformed - ed25519 +crypto.generateKeyPairSync('ed25519', { + hash: 'sha256' +}); + +// Should be transformed - rsa-pss +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha256' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/promisified-wrappers.js b/recipes/crypto-rsa-pss-update/tests/expected/promisified-wrappers.js new file mode 100644 index 00000000..e888fdfb --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/promisified-wrappers.js @@ -0,0 +1,63 @@ +const crypto = require('crypto'); +const util = require('util'); + +// Case 1: Basic promisified wrapper +const generateKeyPairAsync = util.promisify(crypto.generateKeyPair); + +generateKeyPairAsync('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha1', + saltLength: 32 +}); + +// Case 2: Promisified sync version +const generateKeyPairSyncAsync = util.promisify(crypto.generateKeyPairSync); + +generateKeyPairSyncAsync('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: 'sha512' +}); + +// Case 3: Different variable name +const keyGenAsync = util.promisify(crypto.generateKeyPair); + +keyGenAsync('rsa-pss', { + mgf1HashAlgorithm: 'sha256' +}); + +// Case 4: Destructured import with promisify +const { generateKeyPair } = require('crypto'); +const generateKeyPairPromise = util.promisify(generateKeyPair); + +generateKeyPairPromise('rsa-pss', { + hashAlgorithm: 'sha1', + mgf1HashAlgorithm: 'sha256' +}); + +// Case 5: Mixed with regular calls +crypto.generateKeyPair('rsa-pss', { + hashAlgorithm: 'sha256' +}); + +// Case 6: Destructured promisify import +const { promisify } = require('util'); +const promisifyGenerate = promisify(crypto.generateKeyPair); + +promisifyGenerate('rsa-pss', { + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha1' +}); + +// Case 7: Mixed destructured imports +const { promisify: p } = require('util'); +const aliasedPromisify = p(crypto.generateKeyPair); + +aliasedPromisify('rsa-pss', { + mgf1HashAlgorithm: 'sha512' +}); + +// Case 8: Non-rsa-pss should not transform +generateKeyPairAsync('rsa', { + hash: 'sha256' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/spread-operators.js b/recipes/crypto-rsa-pss-update/tests/expected/spread-operators.js new file mode 100644 index 00000000..fe121c3b --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/spread-operators.js @@ -0,0 +1,26 @@ +const crypto = require('crypto'); + +const baseOptions = { + modulusLength: 2048, + saltLength: 32 +}; + +const hashOptions = { + hashAlgorithm: 'sha256' +}; + +// Spread operator with hash option +crypto.generateKeyPair('rsa-pss', { + ...baseOptions, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha1' +}, (err, publicKey, privateKey) => { + console.log('Generated keys'); +}); + +// Spread with existing hash options +crypto.generateKeyPairSync('rsa-pss', { + ...baseOptions, + ...hashOptions, + mgf1HashAlgorithm: 'sha1' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/template-literals.js b/recipes/crypto-rsa-pss-update/tests/expected/template-literals.js new file mode 100644 index 00000000..ff8d986e --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/template-literals.js @@ -0,0 +1,19 @@ +const crypto = require('crypto'); + +const algorithm = 'sha256'; +const mgfAlgorithm = 'sha1'; + +// Template literal test case +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: `${algorithm}`, + saltLength: 32 +}, (err, publicKey, privateKey) => { + console.log('Generated keys'); +}); + +// Complex template literal test case +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + mgf1HashAlgorithm: `${mgfAlgorithm}-mgf` +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/ternary-operators.js b/recipes/crypto-rsa-pss-update/tests/expected/ternary-operators.js new file mode 100644 index 00000000..8bd6a8ce --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/ternary-operators.js @@ -0,0 +1,39 @@ +const crypto = require('crypto'); + +const isDev = process.env.NODE_ENV === 'development'; +const useStrong = true; + +// Case 1: Ternary operator in options object - currently no change (limitation) +const options1 = isDev ? + { hash: 'sha1', mgf1Hash: 'sha1' } : + { hash: 'sha256', mgf1Hash: 'sha256' }; + +crypto.generateKeyPair('rsa-pss', options1, (err, publicKey, privateKey) => { + console.log('Generated with conditional options'); +}); + +// Case 2: Conditional within object properties - actually WORKS! +const options2 = { + modulusLength: 2048, + hashAlgorithm: useStrong ? 'sha256' : 'sha1', + mgf1HashAlgorithm: isDev ? 'sha1' : 'sha256' +}; + +crypto.generateKeyPairSync('rsa-pss', options2); + +// Case 3: Logical AND/OR expressions - actually WORKS! +const options3 = { + hashAlgorithm: (isDev && 'sha1') || 'sha256', + mgf1HashAlgorithm: useStrong && 'sha256' +}; + +crypto.generateKeyPair('rsa-pss', options3); + +// Case 4: Function call in conditional - inline objects WORK! +function getHashOptions() { + return { hash: 'sha256', mgf1Hash: 'sha1' }; +} + +crypto.generateKeyPairSync('rsa-pss', + isDev ? getHashOptions() : { hashAlgorithm: 'sha1' } +); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/this-property.js b/recipes/crypto-rsa-pss-update/tests/expected/this-property.js new file mode 100644 index 00000000..95b51d35 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/this-property.js @@ -0,0 +1,61 @@ +const crypto = require('crypto'); + +// Class with this.property patterns +class CryptoHelper { + constructor() { + this.options = { + modulusLength: 2048, + hashAlgorithm: 'sha256', + mgf1HashAlgorithm: 'sha1', + saltLength: 32 + }; + + this.advancedOptions = { + hashAlgorithm: 'sha512', + algorithm: 'RSA-PSS' + }; + } + + generateKey() { + crypto.generateKeyPair('rsa-pss', this.options, (err, publicKey, privateKey) => { + console.log('Generated with this.options'); + }); + } + + generateAdvancedKey() { + return crypto.generateKeyPairSync('rsa-pss', this.advancedOptions); + } +} + +// Object method with this.property +const cryptoService = { + cryptoConfig: { + modulusLength: 2048, + mgf1Hash: 'sha256' + }, + + init() { + this.cryptoConfig = { + modulusLength: 4096, + hashAlgorithm: 'sha384', + mgf1HashAlgorithm: 'sha512' + }; + }, + + generate() { + crypto.generateKeyPair('rsa-pss', this.cryptoConfig, () => {}); + } +}; + +// Mixed case - some this.property, some not +function testMixed() { + const localOptions = { hashAlgorithm: 'sha256' }; + + this.globalOptions = { + hashAlgorithm: 'sha512', + mgf1HashAlgorithm: 'sha1' + }; + + crypto.generateKeyPair('rsa-pss', localOptions); + crypto.generateKeyPairSync('rsa-pss', this.globalOptions); +} \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/variable-key-type.js b/recipes/crypto-rsa-pss-update/tests/expected/variable-key-type.js new file mode 100644 index 00000000..baebe6b2 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/variable-key-type.js @@ -0,0 +1,20 @@ +const crypto = require('crypto'); + +const keyType = 'rsa-pss'; +const algorithm = 'sha256'; + +// Variable type parameter - should be detected and transformed ideally +crypto.generateKeyPair(keyType, { + modulusLength: 2048, + hashAlgorithm: algorithm, + saltLength: 32 +}, (err, publicKey, privateKey) => { + console.log('Generated keys with variable type'); +}); + +// Another variable case +const rsaPssType = 'rsa-pss'; +crypto.generateKeyPairSync(rsaPssType, { + modulusLength: 2048, + mgf1HashAlgorithm: 'sha1' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/expected/variable-value.js b/recipes/crypto-rsa-pss-update/tests/expected/variable-value.js new file mode 100644 index 00000000..97d2b664 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/expected/variable-value.js @@ -0,0 +1,17 @@ +const crypto = require('crypto'); + +const algo = 'sha256'; +const hashAlgo = 'sha1'; + +// Variable value case +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + mgf1HashAlgorithm: algo +}); + +// Multiple variable values +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hashAlgorithm: hashAlgo, + mgf1HashAlgorithm: algo +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/basic.js b/recipes/crypto-rsa-pss-update/tests/input/basic.js new file mode 100644 index 00000000..ef3ffe78 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/basic.js @@ -0,0 +1,17 @@ +const crypto = require('crypto'); + +// Basic case with hash option only +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hash: 'sha256', + saltLength: 32 +}, (err, publicKey, privateKey) => { + console.log('Generated keys'); +}); + +// Both hash and mgf1Hash options together +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + hash: 'sha256', + mgf1Hash: 'sha1' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/complex-ast-patterns.js b/recipes/crypto-rsa-pss-update/tests/input/complex-ast-patterns.js new file mode 100644 index 00000000..06e0ce63 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/complex-ast-patterns.js @@ -0,0 +1,53 @@ +const crypto = require('crypto'); + +// Test case 1: member_expression - accessing constants +const constants = { + SECURE_HASH: 'sha512', + MGF_HASH: 'sha256' +}; + +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hash: constants.SECURE_HASH, // ← member_expression + saltLength: 32 +}); + +// Test case 2: call_expression - functions returning algorithms +function getSecureHash() { + return 'sha256'; +} + +function getMgfHash() { + return 'sha1'; +} + +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + hash: getSecureHash(), // ← call_expression + mgf1Hash: getMgfHash() // ← call_expression +}); + +// Test case 3: binary_expression - string concatenation +const bitLength = '256'; +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hash: 'sha' + bitLength, // ← binary_expression + mgf1Hash: 'sha' + '1' // ← binary_expression +}); + +// Test case 4: conditional_expression - ternary operator +const isProduction = true; +const useStrongSecurity = false; + +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + hash: isProduction ? 'sha512' : 'sha256', // ← conditional_expression + mgf1Hash: useStrongSecurity ? 'sha256' : 'sha1' // ← conditional_expression +}); + +// Test case 5: Complex mixed cases +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hash: process.env.NODE_ENV === 'production' ? crypto.constants.defaultHash || 'sha512' : 'sha256', // ← multiple types + saltLength: 32 +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/computed-properties.js b/recipes/crypto-rsa-pss-update/tests/input/computed-properties.js new file mode 100644 index 00000000..926ef941 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/computed-properties.js @@ -0,0 +1,45 @@ +const crypto = require('crypto'); + +// Computed property names - ideally should be transformed +const hashKey = 'hash'; +const mgfKey = 'mgf1Hash'; + +// Case 1: Computed property with bracket notation +const options1 = { + modulusLength: 2048, + [hashKey]: 'sha256', // Ideally should transform to hashAlgorithm + mgf1Hash: 'sha1' // Should transform (static) +}; + +crypto.generateKeyPair('rsa-pss', options1, (err, publicKey, privateKey) => { + console.log('Generated with computed hash property'); +}); + +// Case 2: Both properties computed +const options2 = { + [hashKey]: 'sha256', // Ideally should transform + [mgfKey]: 'sha1' // Ideally should transform +}; + +crypto.generateKeyPairSync('rsa-pss', options2); + +// Case 3: Mixed static and computed +const options3 = { + hash: 'sha256', // Should transform + [mgfKey]: 'sha1', // Ideally should transform + modulusLength: 2048 +}; + +crypto.generateKeyPair('rsa-pss', options3); + +// Case 4: Dynamic key construction +const propertyName = 'hash'; +const anotherProperty = 'mgf1' + 'Hash'; + +const options4 = { + [propertyName]: 'sha256', + [anotherProperty]: 'sha1', + modulusLength: 2048 +}; + +crypto.generateKeyPairSync('rsa-pss', options4); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/destructured.js b/recipes/crypto-rsa-pss-update/tests/input/destructured.js new file mode 100644 index 00000000..d0c0b9ed --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/destructured.js @@ -0,0 +1,67 @@ +// Comprehensive import patterns test +import { generateKeyPair } from 'crypto'; +import crypto as nodeCrypto from 'crypto'; +import { generateKeyPair as keyGen } from 'node:crypto'; +const { generateKeyPairSync } = require('node:crypto'); +const { generateKeyPair: aliasedGenerateKeyPair, generateKeyPairSync: foo } = require('crypto'); +const cryptoLib = require('crypto'); + +// ES6 import destructuring +generateKeyPair('rsa-pss', { + modulusLength: 2048, + hash: 'sha256' +}, (err, publicKey, privateKey) => { + console.log('ES6 import'); +}); + +// CommonJS destructuring +generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + mgf1Hash: 'sha1' +}); + +// Aliased destructuring with meaningful names +aliasedGenerateKeyPair('rsa-pss', { + modulusLength: 2048, + hash: 'sha512', + mgf1Hash: 'sha256' +}); + +// Aliased destructuring with short names +foo('rsa-pss', { + modulusLength: 2048, + hash: 'sha384' +}); + +// Namespace import with alias +nodeCrypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hash: 'sha256', + saltLength: 32 +}, (err, publicKey, privateKey) => { + console.log('Generated keys with aliased import'); +}); + +// Function alias from destructuring +keyGen('rsa-pss', { + modulusLength: 2048, + mgf1Hash: 'sha1' +}); + +// Variable assignment +cryptoLib.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + hash: 'sha512' +}); + +// Mixed CJS + ESM imports (consolidated from mixed-imports.js) +import { constants, randomBytes } from 'crypto'; + +// Using constants from ESM import +const optionsMixed = { + hash: constants.defaultCipherName || 'sha256', + mgf1Hash: 'sha1', + modulusLength: 2048 +}; + +cryptoLib.generateKeyPair('rsa-pss', optionsMixed); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/dynamic-options.js b/recipes/crypto-rsa-pss-update/tests/input/dynamic-options.js new file mode 100644 index 00000000..419db6e4 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/dynamic-options.js @@ -0,0 +1,22 @@ +const crypto = require('crypto'); + +// Dynamic options object - now supported by codemod +const options = { + modulusLength: 2048, + hash: 'sha256', + saltLength: 32 +}; + +crypto.generateKeyPair('rsa-pss', options, (err, publicKey, privateKey) => { + console.log('Generated keys with dynamic options'); +}); + +// Function returning options +function getKeyOptions() { + return { + modulusLength: 2048, + mgf1Hash: 'sha1' + }; +} + +crypto.generateKeyPairSync('rsa-pss', getKeyOptions()); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/method-chaining.js b/recipes/crypto-rsa-pss-update/tests/input/method-chaining.js new file mode 100644 index 00000000..6579829f --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/method-chaining.js @@ -0,0 +1,63 @@ +const crypto = require('crypto'); +const { promisify } = require('util'); + +// Case 1: Promise chaining with crypto.generateKeyPair +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hash: 'sha256', + mgf1Hash: 'sha1' +}, (err, publicKey, privateKey) => { + console.log('Generated keys'); +}); + +// Case 2: Method chaining with promisified version +const generateKeyPairAsync = promisify(crypto.generateKeyPair); + +generateKeyPairAsync('rsa-pss', { + hash: 'sha256', + mgf1Hash: 'sha1', + modulusLength: 2048 +}).then(({ publicKey, privateKey }) => { + console.log('Keys generated via promise'); +}).catch(console.error); + +// Case 3: Async/await with inline options +async function generateKeys() { + try { + const { publicKey, privateKey } = await generateKeyPairAsync('rsa-pss', { + hash: 'sha256', + mgf1Hash: 'sha1' + }); + return { publicKey, privateKey }; + } catch (error) { + console.error('Key generation failed:', error); + } +} + +// Case 4: Class method with fluent API pattern +class KeyGenerator { + constructor() { + this.options = {}; + } + + setHash(algorithm) { + this.options.hash = algorithm; + return this; + } + + setMgf1Hash(algorithm) { + this.options.mgf1Hash = algorithm; + return this; + } + + async generate() { + return generateKeyPairAsync('rsa-pss', this.options); + } +} + +// Usage +new KeyGenerator() + .setHash('sha256') + .setMgf1Hash('sha1') + .generate() + .then(console.log); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/namespace-import.mjs b/recipes/crypto-rsa-pss-update/tests/input/namespace-import.mjs new file mode 100644 index 00000000..10386c2d --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/namespace-import.mjs @@ -0,0 +1,16 @@ +import * as crypto from 'node:crypto'; + +// This should be transformed but currently fails +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hash: 'sha256', + saltLength: 32 +}, (err, publicKey, privateKey) => { + console.log('Generated keys'); +}); + +// This should also be transformed +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + mgf1Hash: 'sha256' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/nested-objects.js b/recipes/crypto-rsa-pss-update/tests/input/nested-objects.js new file mode 100644 index 00000000..522cbfb6 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/nested-objects.js @@ -0,0 +1,22 @@ +const crypto = require('crypto'); + +// Nested object structure test +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + options: { + hash: 'sha256', + mgf1Hash: 'sha1' + }, + saltLength: 32 +}, (err, publicKey, privateKey) => { + console.log('Generated keys'); +}); + +// Mixed nested and direct properties +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + hash: 'sha256', + advanced: { + mgf1Hash: 'sha1' + } +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/non-rsa-pss.js b/recipes/crypto-rsa-pss-update/tests/input/non-rsa-pss.js new file mode 100644 index 00000000..c1189ddc --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/non-rsa-pss.js @@ -0,0 +1,20 @@ +const crypto = require('crypto'); + +// Should NOT be transformed - different key type +crypto.generateKeyPair('rsa', { + modulusLength: 2048, + hash: 'sha256' +}, (err, publicKey, privateKey) => { + // callback +}); + +// Should NOT be transformed - ed25519 +crypto.generateKeyPairSync('ed25519', { + hash: 'sha256' +}); + +// Should be transformed - rsa-pss +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hash: 'sha256' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/promisified-wrappers.js b/recipes/crypto-rsa-pss-update/tests/input/promisified-wrappers.js new file mode 100644 index 00000000..0a7936d9 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/promisified-wrappers.js @@ -0,0 +1,63 @@ +const crypto = require('crypto'); +const util = require('util'); + +// Case 1: Basic promisified wrapper +const generateKeyPairAsync = util.promisify(crypto.generateKeyPair); + +generateKeyPairAsync('rsa-pss', { + modulusLength: 2048, + hash: 'sha256', + mgf1Hash: 'sha1', + saltLength: 32 +}); + +// Case 2: Promisified sync version +const generateKeyPairSyncAsync = util.promisify(crypto.generateKeyPairSync); + +generateKeyPairSyncAsync('rsa-pss', { + modulusLength: 2048, + hash: 'sha512' +}); + +// Case 3: Different variable name +const keyGenAsync = util.promisify(crypto.generateKeyPair); + +keyGenAsync('rsa-pss', { + mgf1Hash: 'sha256' +}); + +// Case 4: Destructured import with promisify +const { generateKeyPair } = require('crypto'); +const generateKeyPairPromise = util.promisify(generateKeyPair); + +generateKeyPairPromise('rsa-pss', { + hash: 'sha1', + mgf1Hash: 'sha256' +}); + +// Case 5: Mixed with regular calls +crypto.generateKeyPair('rsa-pss', { + hash: 'sha256' +}); + +// Case 6: Destructured promisify import +const { promisify } = require('util'); +const promisifyGenerate = promisify(crypto.generateKeyPair); + +promisifyGenerate('rsa-pss', { + hash: 'sha256', + mgf1Hash: 'sha1' +}); + +// Case 7: Mixed destructured imports +const { promisify: p } = require('util'); +const aliasedPromisify = p(crypto.generateKeyPair); + +aliasedPromisify('rsa-pss', { + mgf1Hash: 'sha512' +}); + +// Case 8: Non-rsa-pss should not transform +generateKeyPairAsync('rsa', { + hash: 'sha256' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/spread-operators.js b/recipes/crypto-rsa-pss-update/tests/input/spread-operators.js new file mode 100644 index 00000000..a3012227 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/spread-operators.js @@ -0,0 +1,26 @@ +const crypto = require('crypto'); + +const baseOptions = { + modulusLength: 2048, + saltLength: 32 +}; + +const hashOptions = { + hash: 'sha256' +}; + +// Spread operator with hash option +crypto.generateKeyPair('rsa-pss', { + ...baseOptions, + hash: 'sha256', + mgf1Hash: 'sha1' +}, (err, publicKey, privateKey) => { + console.log('Generated keys'); +}); + +// Spread with existing hash options +crypto.generateKeyPairSync('rsa-pss', { + ...baseOptions, + ...hashOptions, + mgf1Hash: 'sha1' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/template-literals.js b/recipes/crypto-rsa-pss-update/tests/input/template-literals.js new file mode 100644 index 00000000..a77802d5 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/template-literals.js @@ -0,0 +1,19 @@ +const crypto = require('crypto'); + +const algorithm = 'sha256'; +const mgfAlgorithm = 'sha1'; + +// Template literal test case +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hash: `${algorithm}`, + saltLength: 32 +}, (err, publicKey, privateKey) => { + console.log('Generated keys'); +}); + +// Complex template literal test case +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + mgf1Hash: `${mgfAlgorithm}-mgf` +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/ternary-operators.js b/recipes/crypto-rsa-pss-update/tests/input/ternary-operators.js new file mode 100644 index 00000000..cf5afebb --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/ternary-operators.js @@ -0,0 +1,39 @@ +const crypto = require('crypto'); + +const isDev = process.env.NODE_ENV === 'development'; +const useStrong = true; + +// Case 1: Ternary operator in options object - currently no change (limitation) +const options1 = isDev ? + { hash: 'sha1', mgf1Hash: 'sha1' } : + { hash: 'sha256', mgf1Hash: 'sha256' }; + +crypto.generateKeyPair('rsa-pss', options1, (err, publicKey, privateKey) => { + console.log('Generated with conditional options'); +}); + +// Case 2: Conditional within object properties - actually WORKS! +const options2 = { + modulusLength: 2048, + hash: useStrong ? 'sha256' : 'sha1', + mgf1Hash: isDev ? 'sha1' : 'sha256' +}; + +crypto.generateKeyPairSync('rsa-pss', options2); + +// Case 3: Logical AND/OR expressions - actually WORKS! +const options3 = { + hash: (isDev && 'sha1') || 'sha256', + mgf1Hash: useStrong && 'sha256' +}; + +crypto.generateKeyPair('rsa-pss', options3); + +// Case 4: Function call in conditional - inline objects WORK! +function getHashOptions() { + return { hash: 'sha256', mgf1Hash: 'sha1' }; +} + +crypto.generateKeyPairSync('rsa-pss', + isDev ? getHashOptions() : { hash: 'sha1' } +); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/this-property.js b/recipes/crypto-rsa-pss-update/tests/input/this-property.js new file mode 100644 index 00000000..a2938a7f --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/this-property.js @@ -0,0 +1,61 @@ +const crypto = require('crypto'); + +// Class with this.property patterns +class CryptoHelper { + constructor() { + this.options = { + modulusLength: 2048, + hash: 'sha256', + mgf1Hash: 'sha1', + saltLength: 32 + }; + + this.advancedOptions = { + hash: 'sha512', + algorithm: 'RSA-PSS' + }; + } + + generateKey() { + crypto.generateKeyPair('rsa-pss', this.options, (err, publicKey, privateKey) => { + console.log('Generated with this.options'); + }); + } + + generateAdvancedKey() { + return crypto.generateKeyPairSync('rsa-pss', this.advancedOptions); + } +} + +// Object method with this.property +const cryptoService = { + cryptoConfig: { + modulusLength: 2048, + mgf1Hash: 'sha256' + }, + + init() { + this.cryptoConfig = { + modulusLength: 4096, + hash: 'sha384', + mgf1Hash: 'sha512' + }; + }, + + generate() { + crypto.generateKeyPair('rsa-pss', this.cryptoConfig, () => {}); + } +}; + +// Mixed case - some this.property, some not +function testMixed() { + const localOptions = { hash: 'sha256' }; + + this.globalOptions = { + hash: 'sha512', + mgf1Hash: 'sha1' + }; + + crypto.generateKeyPair('rsa-pss', localOptions); + crypto.generateKeyPairSync('rsa-pss', this.globalOptions); +} \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/variable-key-type.js b/recipes/crypto-rsa-pss-update/tests/input/variable-key-type.js new file mode 100644 index 00000000..72cb78c3 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/variable-key-type.js @@ -0,0 +1,20 @@ +const crypto = require('crypto'); + +const keyType = 'rsa-pss'; +const algorithm = 'sha256'; + +// Variable type parameter - should be detected and transformed ideally +crypto.generateKeyPair(keyType, { + modulusLength: 2048, + hash: algorithm, + saltLength: 32 +}, (err, publicKey, privateKey) => { + console.log('Generated keys with variable type'); +}); + +// Another variable case +const rsaPssType = 'rsa-pss'; +crypto.generateKeyPairSync(rsaPssType, { + modulusLength: 2048, + mgf1Hash: 'sha1' +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/tests/input/variable-value.js b/recipes/crypto-rsa-pss-update/tests/input/variable-value.js new file mode 100644 index 00000000..11bcbf7a --- /dev/null +++ b/recipes/crypto-rsa-pss-update/tests/input/variable-value.js @@ -0,0 +1,17 @@ +const crypto = require('crypto'); + +const algo = 'sha256'; +const hashAlgo = 'sha1'; + +// Variable value case +crypto.generateKeyPairSync('rsa-pss', { + modulusLength: 2048, + mgf1Hash: algo +}); + +// Multiple variable values +crypto.generateKeyPair('rsa-pss', { + modulusLength: 2048, + hash: hashAlgo, + mgf1Hash: algo +}); \ No newline at end of file diff --git a/recipes/crypto-rsa-pss-update/workflow.yaml b/recipes/crypto-rsa-pss-update/workflow.yaml new file mode 100644 index 00000000..1ddac483 --- /dev/null +++ b/recipes/crypto-rsa-pss-update/workflow.yaml @@ -0,0 +1,30 @@ +# yaml-language-server: $schema= +# https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + runtime: + type: direct + steps: + - name: >- + Replace deprecated RSA-PSS crypto options `hash` to `hashAlgorithm` + and `mgf1Hash` to `mgf1HashAlgorithm`. + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.cts" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript