From 96520019c941a4d37b8b3f81781e27c0b2d8926f Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:43:08 +0100 Subject: [PATCH 1/8] WIP --- package-lock.json | 18 + .../crypto-createcipheriv-migration/README.md | 34 ++ .../codemod.yaml | 22 + .../package.json | 24 + .../src/workflow.ts | 427 ++++++++++++++++++ .../tests/expected/commonjs-alias.js | 12 + .../commonjs-decipher-destructured.js | 10 + .../expected/commonjs-decipher-namespace.js | 10 + .../tests/expected/commonjs-destructured.js | 10 + .../tests/expected/commonjs-namespace.js | 12 + .../tests/expected/commonjs-options.js | 10 + .../tests/expected/esm-named-decipher.js | 10 + .../tests/expected/esm-namespace.js | 10 + .../tests/input/commonjs-alias.js | 5 + .../input/commonjs-decipher-destructured.js | 3 + .../input/commonjs-decipher-namespace.js | 3 + .../tests/input/commonjs-destructured.js | 3 + .../tests/input/commonjs-namespace.js | 5 + .../tests/input/commonjs-options.js | 3 + .../tests/input/esm-named-decipher.js | 3 + .../tests/input/esm-namespace.js | 3 + .../workflow.yaml | 25 + 22 files changed, 662 insertions(+) create mode 100644 recipes/crypto-createcipheriv-migration/README.md create mode 100644 recipes/crypto-createcipheriv-migration/codemod.yaml create mode 100644 recipes/crypto-createcipheriv-migration/package.json create mode 100644 recipes/crypto-createcipheriv-migration/src/workflow.ts create mode 100644 recipes/crypto-createcipheriv-migration/tests/expected/commonjs-alias.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/expected/commonjs-decipher-destructured.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/expected/commonjs-decipher-namespace.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/expected/commonjs-destructured.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/expected/commonjs-namespace.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/expected/commonjs-options.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/expected/esm-named-decipher.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/expected/esm-namespace.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/input/commonjs-alias.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/input/commonjs-decipher-destructured.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/input/commonjs-decipher-namespace.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/input/commonjs-destructured.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/input/commonjs-namespace.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/input/commonjs-options.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/input/esm-named-decipher.js create mode 100644 recipes/crypto-createcipheriv-migration/tests/input/esm-namespace.js create mode 100644 recipes/crypto-createcipheriv-migration/workflow.yaml diff --git a/package-lock.json b/package-lock.json index 944bf453..a4ac14c1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -415,6 +415,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -1469,6 +1470,10 @@ "resolved": "recipes/create-require-from-path", "link": true }, + "node_modules/@nodejs/crypto-createcipheriv-migration": { + "resolved": "recipes/crypto-createcipheriv-migration", + "link": true + }, "node_modules/@nodejs/crypto-fips-to-getFips": { "resolved": "recipes/crypto-fips-to-getFips", "link": true @@ -1551,6 +1556,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -2057,6 +2063,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001733", "electron-to-chromium": "^1.5.199", @@ -4287,6 +4294,17 @@ "@codemod.com/jssg-types": "^1.0.9" } }, + "recipes/crypto-createcipheriv-migration": { + "name": "@nodejs/crypto-createcipheriv-migration", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + } + }, "recipes/crypto-fips": { "name": "@nodejs/crypto-fips", "version": "1.0.0", diff --git a/recipes/crypto-createcipheriv-migration/README.md b/recipes/crypto-createcipheriv-migration/README.md new file mode 100644 index 00000000..29ffb93f --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/README.md @@ -0,0 +1,34 @@ +# crypto-createcipheriv-migration + +> Migrates deprecated `crypto.createCipher()` / `crypto.createDecipher()` usage to the supported `crypto.createCipheriv()` / `crypto.createDecipheriv()` APIs with explicit key derivation and IV handling. + +## Why? + +Node.js removed `crypto.createCipher()` and `crypto.createDecipher()` in v22.0.0 (DEP0106). The legacy helpers derived keys with MD5 and no salt, and silently reused static IVs. This codemod replaces those calls with the modern, explicit APIs and scaffolds secure key derivation and IV management. + +## What it does + +- Detects CommonJS and ESM imports of `crypto` (including destructured bindings). +- Replaces invocations of `createCipher()` / `createDecipher()` with `createCipheriv()` / `createDecipheriv()`. +- Inserts scaffolding that derives keys with `crypto.scryptSync()` and generates random salts and IVs. +- Reminds developers to persist salt + IV for decryption and to adjust key/IV lengths per algorithm. +- Updates destructured imports to include the new helpers (`createCipheriv`, `createDecipheriv`, `randomBytes`, `scryptSync`). + +## Example + +```diff +-const cipher = crypto.createCipher(algorithm, password); ++const cipher = (() => { ++ const __dep0106Salt = crypto.randomBytes(16); ++ const __dep0106Key = crypto.scryptSync(password, __dep0106Salt, 32); ++ const __dep0106Iv = crypto.randomBytes(16); ++ // DEP0106: Persist __dep0106Salt and __dep0106Iv alongside the ciphertext so it can be decrypted later. ++ return crypto.createCipheriv(algorithm, __dep0106Key, __dep0106Iv); ++})(); +``` + +## Caveats + +- The codemod cannot guarantee algorithm-specific key/IV sizes. Review the generated `scryptSync` length and IV length defaults and adjust as needed. +- Decryption snippets include placeholders (`Buffer.alloc(16)`) that must be replaced with the salt and IV stored during encryption. +- If your project already wraps key derivation logic, you may prefer to adapt the generated scaffolding to call existing helpers. diff --git a/recipes/crypto-createcipheriv-migration/codemod.yaml b/recipes/crypto-createcipheriv-migration/codemod.yaml new file mode 100644 index 00000000..055df724 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/codemod.yaml @@ -0,0 +1,22 @@ +schema_version: "1.0" +name: "@nodejs/crypto-createcipheriv-migration" +version: 1.0.0 +description: Replace removed `crypto.createCipher()`/`createDecipher()` with `crypto.createCipheriv()`/`createDecipheriv()` and secure key derivation (DEP0106) +author: Augustin Mauroy +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - crypto + +registry: + access: public + visibility: public diff --git a/recipes/crypto-createcipheriv-migration/package.json b/recipes/crypto-createcipheriv-migration/package.json new file mode 100644 index 00000000..2dc423b3 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/package.json @@ -0,0 +1,24 @@ +{ + "name": "@nodejs/crypto-createcipheriv-migration", + "version": "1.0.0", + "description": "Migrate deprecated crypto.createCipher()/createDecipher() (DEP0106) to crypto.createCipheriv()/createDecipheriv() with secure key derivation.", + "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-createcipheriv-migration", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Augustin Mauroy", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/crypto-createcipheriv-migration/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + }, + "dependencies": { + "@nodejs/codemod-utils": "*" + } +} diff --git a/recipes/crypto-createcipheriv-migration/src/workflow.ts b/recipes/crypto-createcipheriv-migration/src/workflow.ts new file mode 100644 index 00000000..128e8b69 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/src/workflow.ts @@ -0,0 +1,427 @@ +import type { Edit, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; +import { + getNodeImportCalls, + 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'; + +type CallKind = 'cipher' | 'decipher'; + +type StatementChange = { + rename: Map; + additions: Set; +}; + +type BindingEntry = { + property: string; + local: string; +}; + +type CollectParams = { + rootNode: SgNode; + statement: SgNode; + binding: string; + kind: CallKind; + edits: Edit[]; + statementChanges: Map, StatementChange>; + seenCallRanges: Set; +}; + +/** + * Transform deprecated crypto.createCipher()/createDecipher() usage to the + * supported crypto.createCipheriv()/createDecipheriv() APIs. + */ +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + const statementChanges = new Map, StatementChange>(); + const seenCallRanges = new Set(); + + for (const statement of collectCryptoStatements(root)) { + const cipherBinding = safeResolveBinding(statement, '$.createCipher'); + if (cipherBinding) { + collectCallEdits({ + rootNode, + statement, + binding: cipherBinding, + kind: 'cipher', + edits, + statementChanges, + seenCallRanges, + }); + } + + const decipherBinding = safeResolveBinding(statement, '$.createDecipher'); + if (decipherBinding) { + collectCallEdits({ + rootNode, + statement, + binding: decipherBinding, + kind: 'decipher', + edits, + statementChanges, + seenCallRanges, + }); + } + } + + for (const [statement, change] of statementChanges) { + const edit = applyStatementChanges(statement, change); + if (edit) edits.push(edit); + } + + if (edits.length === 0) return null; + + return rootNode.commitEdits(edits); +} + +function collectCallEdits({ + rootNode, + statement, + binding, + kind, + edits, + statementChanges, + seenCallRanges, +}: CollectParams) { + const patterns = [ + `${binding}($ALGORITHM, $PASSWORD, $OPTIONS)`, + `${binding}($ALGORITHM, $PASSWORD)`, + ]; + + const calls = rootNode.findAll({ + rule: { + any: patterns.map((pattern) => ({ pattern })), + kind: 'call_expression', + }, + }); + + for (const call of calls) { + const rangeKey = getRangeKey(call); + if (seenCallRanges.has(rangeKey)) continue; + seenCallRanges.add(rangeKey); + + const algorithmNode = call.getMatch('ALGORITHM'); + const passwordNode = call.getMatch('PASSWORD'); + + if (!algorithmNode || !passwordNode) continue; + + const algorithm = algorithmNode.text().trim(); + const password = passwordNode.text().trim(); + if (!algorithm || !password) continue; + + const optionsText = call.getMatch('OPTIONS')?.text()?.trim(); + + const replacement = + kind === 'cipher' + ? buildCipherReplacement({ + binding, + algorithm, + password, + options: optionsText, + }) + : buildDecipherReplacement({ + binding, + algorithm, + password, + options: optionsText, + }); + + edits.push(call.replace(replacement)); + + if (isDestructuredStatement(statement)) { + const change = ensureStatementChange(statementChanges, statement); + // Ensure the binding points to the iv-based API + const sourceName = kind === 'cipher' ? 'createCipher' : 'createDecipher'; + const targetName = `${sourceName}iv`; + change.rename.set(sourceName, targetName); + if (kind === 'cipher') { + change.additions.add('randomBytes'); + } + change.additions.add('scryptSync'); + } + } +} + +function buildCipherReplacement(params: { + binding: string; + algorithm: string; + password: string; + options?: string; +}): string { + const { binding, algorithm, password, options } = params; + const randomBytesCall = getMemberAccess(binding, 'randomBytes'); + const scryptCall = getMemberAccess(binding, 'scryptSync'); + const cipherCall = getCallableBinding(binding, 'createCipheriv'); + + const lines = [ + '(() => {', + '\tconst __dep0106Salt = ' + randomBytesCall + '(16);', + '\tconst __dep0106Key = ' + + scryptCall + + '(' + + password + + ', __dep0106Salt, 32);', + '\tconst __dep0106Iv = ' + randomBytesCall + '(16);', + '\t// DEP0106: Persist __dep0106Salt and __dep0106Iv with the ciphertext so it can be decrypted later.', + '\t// DEP0106: Adjust the derived key length (32 bytes) and IV length to match the chosen algorithm.', + '\treturn ' + + cipherCall + + '(' + + algorithm + + ', __dep0106Key, __dep0106Iv' + + (options ? ', ' + options : '') + + ');', + '})()', + ]; + + return lines.join('\n'); +} + +function buildDecipherReplacement(params: { + binding: string; + algorithm: string; + password: string; + options?: string; +}): string { + const { binding, algorithm, password, options } = params; + const scryptCall = getMemberAccess(binding, 'scryptSync'); + const decipherCall = getCallableBinding(binding, 'createDecipheriv'); + + const lines = [ + '(() => {', + '\t// DEP0106: Replace the placeholders below with the salt and IV that were stored with the ciphertext.', + '\tconst __dep0106Salt = /* TODO: stored salt Buffer */ Buffer.alloc(16);', + '\tconst __dep0106Iv = /* TODO: stored IV Buffer */ Buffer.alloc(16);', + '\tconst __dep0106Key = ' + + scryptCall + + '(' + + password + + ', __dep0106Salt, 32);', + '\t// DEP0106: Ensure __dep0106Salt and __dep0106Iv match the values used during encryption.', + '\treturn ' + + decipherCall + + '(' + + algorithm + + ', __dep0106Key, __dep0106Iv' + + (options ? ', ' + options : '') + + ');', + '})()', + ]; + + return lines.join('\n'); +} + +function getCallableBinding(binding: string, target: string): string { + const lastDot = binding.lastIndexOf('.'); + if (lastDot === -1) { + return binding; + } + return binding.slice(0, lastDot) + '.' + target; +} + +function getMemberAccess(binding: string, member: string): string { + const lastDot = binding.lastIndexOf('.'); + if (lastDot === -1) { + return member; + } + return binding.slice(0, lastDot) + '.' + member; +} + +function isDestructuredStatement(statement: SgNode): boolean { + return Boolean( + statement.find({ rule: { kind: 'object_pattern' } }) || + statement.find({ rule: { kind: 'named_imports' } }), + ); +} + +function ensureStatementChange( + statementChanges: Map, StatementChange>, + statement: SgNode, +): StatementChange { + let change = statementChanges.get(statement); + if (!change) { + change = { rename: new Map(), additions: new Set() }; + statementChanges.set(statement, change); + } + return change; +} + +function applyStatementChanges( + statement: SgNode, + change: StatementChange, +): Edit | undefined { + if (change.rename.size === 0 && change.additions.size === 0) { + return undefined; + } + + if ( + statement.kind() === 'import_statement' || + statement.kind() === 'import_clause' + ) { + return updateImportSpecifiers(statement, change); + } + + if (statement.find({ rule: { kind: 'object_pattern' } })) { + return updateRequirePattern(statement, change); + } + + return undefined; +} + +function updateImportSpecifiers( + statement: SgNode, + change: StatementChange, +): Edit | undefined { + const clause = + statement.kind() === 'import_clause' + ? statement + : statement.find({ rule: { kind: 'import_clause' } }); + if (!clause) return undefined; + + const namedImports = clause.find({ rule: { kind: 'named_imports' } }); + if (!namedImports) return undefined; + + const specNodes = namedImports.findAll({ + rule: { kind: 'import_specifier' }, + }); + if (specNodes.length === 0) return undefined; + + const entries: BindingEntry[] = specNodes.map((spec) => + parseImportSpecifier(spec.text()), + ); + let modified = false; + + for (const entry of entries) { + const newProperty = change.rename.get(entry.property); + if (newProperty && newProperty !== entry.property) { + entry.property = newProperty; + modified = true; + } + } + + for (const addition of change.additions) { + const exists = entries.some( + (entry) => entry.property === addition || entry.local === addition, + ); + if (!exists) { + entries.push({ property: addition, local: addition }); + modified = true; + } + } + + if (!modified) return undefined; + + const rendered = entries + .map((entry) => + entry.property === entry.local + ? entry.property + : `${entry.property} as ${entry.local}`, + ) + .join(', '); + + return namedImports.replace(`{ ${rendered} }`); +} + +function updateRequirePattern( + statement: SgNode, + change: StatementChange, +): Edit | undefined { + const objectPattern = statement.find({ rule: { kind: 'object_pattern' } }); + if (!objectPattern) return undefined; + + const specNodes = objectPattern.findAll({ + rule: { + any: [ + { kind: 'pair_pattern' }, + { kind: 'shorthand_property_identifier_pattern' }, + ], + }, + }); + if (specNodes.length === 0) return undefined; + + const entries: BindingEntry[] = specNodes.map((spec) => + parseRequireSpecifier(spec.text()), + ); + let modified = false; + + for (const entry of entries) { + const newProperty = change.rename.get(entry.property); + if (newProperty && newProperty !== entry.property) { + entry.property = newProperty; + modified = true; + } + } + + for (const addition of change.additions) { + const exists = entries.some( + (entry) => entry.property === addition || entry.local === addition, + ); + if (!exists) { + entries.push({ property: addition, local: addition }); + modified = true; + } + } + + if (!modified) return undefined; + + const rendered = entries + .map((entry) => + entry.property === entry.local + ? entry.property + : `${entry.property}: ${entry.local}`, + ) + .join(', '); + + return objectPattern.replace(`{ ${rendered} }`); +} + +function parseImportSpecifier(text: string): BindingEntry { + const parts = text + .split(/\s+as\s+/) + .map((value) => value.trim()) + .filter(Boolean); + if (parts.length === 2) { + return { property: parts[0], local: parts[1] }; + } + const name = parts[0] ?? text.trim(); + return { property: name, local: name }; +} + +function parseRequireSpecifier(text: string): BindingEntry { + const parts = text + .split(':') + .map((value) => value.trim()) + .filter(Boolean); + if (parts.length === 2) { + return { property: parts[0], local: parts[1] }; + } + const name = parts[0] ?? text.trim(); + return { property: name, local: name }; +} + +function collectCryptoStatements(root: SgRoot): SgNode[] { + return [ + ...getNodeImportStatements(root, 'crypto'), + ...getNodeImportCalls(root, 'crypto'), + ...getNodeRequireCalls(root, 'crypto'), + ]; +} + +function safeResolveBinding( + node: SgNode, + path: string, +): string | undefined { + try { + return resolveBindingPath(node, path) ?? undefined; + } catch { + return undefined; + } +} + +function getRangeKey(node: SgNode): string { + const range = node.range(); + return `${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}`; +} diff --git a/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-alias.js b/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-alias.js new file mode 100644 index 00000000..f7f41d99 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-alias.js @@ -0,0 +1,12 @@ +const { createCipheriv: makeCipher, randomBytes, scryptSync } = require("node:crypto"); + +function wrap(password) { + return (() => { + const __dep0106Salt = randomBytes(16); + const __dep0106Key = scryptSync(password, __dep0106Salt, 32); + const __dep0106Iv = randomBytes(16); + // DEP0106: Persist __dep0106Salt and __dep0106Iv with the ciphertext so it can be decrypted later. + // DEP0106: Adjust the derived key length (32 bytes) and IV length to match the chosen algorithm. + return makeCipher("aes-192-cbc", __dep0106Key, __dep0106Iv); +})(); +} diff --git a/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-decipher-destructured.js b/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-decipher-destructured.js new file mode 100644 index 00000000..68cb18d1 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-decipher-destructured.js @@ -0,0 +1,10 @@ +const { createDecipheriv: createDecipher, scryptSync } = require("node:crypto"); + +const decipher = (() => { + // DEP0106: Replace the placeholders below with the salt and IV that were stored with the ciphertext. + const __dep0106Salt = /* TODO: stored salt Buffer */ Buffer.alloc(16); + const __dep0106Iv = /* TODO: stored IV Buffer */ Buffer.alloc(16); + const __dep0106Key = scryptSync("secret", __dep0106Salt, 32); + // DEP0106: Ensure __dep0106Salt and __dep0106Iv match the values used during encryption. + return createDecipher("aes-192-cbc", __dep0106Key, __dep0106Iv); +})(); diff --git a/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-decipher-namespace.js b/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-decipher-namespace.js new file mode 100644 index 00000000..299735b5 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-decipher-namespace.js @@ -0,0 +1,10 @@ +const crypto = require("crypto"); + +const decipher = (() => { + // DEP0106: Replace the placeholders below with the salt and IV that were stored with the ciphertext. + const __dep0106Salt = /* TODO: stored salt Buffer */ Buffer.alloc(16); + const __dep0106Iv = /* TODO: stored IV Buffer */ Buffer.alloc(16); + const __dep0106Key = crypto.scryptSync("pw", __dep0106Salt, 32); + // DEP0106: Ensure __dep0106Salt and __dep0106Iv match the values used during encryption. + return crypto.createDecipheriv("aes-256-cbc", __dep0106Key, __dep0106Iv); +})(); diff --git a/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-destructured.js b/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-destructured.js new file mode 100644 index 00000000..a145d728 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-destructured.js @@ -0,0 +1,10 @@ +const { createCipheriv: createCipher, randomBytes, scryptSync } = require("node:crypto"); + +const cipher = (() => { + const __dep0106Salt = randomBytes(16); + const __dep0106Key = scryptSync("password123", __dep0106Salt, 32); + const __dep0106Iv = randomBytes(16); + // DEP0106: Persist __dep0106Salt and __dep0106Iv with the ciphertext so it can be decrypted later. + // DEP0106: Adjust the derived key length (32 bytes) and IV length to match the chosen algorithm. + return createCipher("aes-128-cbc", __dep0106Key, __dep0106Iv); +})(); diff --git a/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-namespace.js b/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-namespace.js new file mode 100644 index 00000000..fe64a8c0 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-namespace.js @@ -0,0 +1,12 @@ +const crypto = require("node:crypto"); + +const algorithm = "aes-256-cbc"; +const password = "s3cret"; +const cipher = (() => { + const __dep0106Salt = crypto.randomBytes(16); + const __dep0106Key = crypto.scryptSync(password, __dep0106Salt, 32); + const __dep0106Iv = crypto.randomBytes(16); + // DEP0106: Persist __dep0106Salt and __dep0106Iv with the ciphertext so it can be decrypted later. + // DEP0106: Adjust the derived key length (32 bytes) and IV length to match the chosen algorithm. + return crypto.createCipheriv(algorithm, __dep0106Key, __dep0106Iv); +})(); diff --git a/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-options.js b/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-options.js new file mode 100644 index 00000000..1d9594f9 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/expected/commonjs-options.js @@ -0,0 +1,10 @@ +const crypto = require("node:crypto"); + +const cipher = (() => { + const __dep0106Salt = crypto.randomBytes(16); + const __dep0106Key = crypto.scryptSync("pw", __dep0106Salt, 32); + const __dep0106Iv = crypto.randomBytes(16); + // DEP0106: Persist __dep0106Salt and __dep0106Iv with the ciphertext so it can be decrypted later. + // DEP0106: Adjust the derived key length (32 bytes) and IV length to match the chosen algorithm. + return crypto.createCipheriv("aes-256-cbc", __dep0106Key, __dep0106Iv, { authTagLength: 16 }); +})(); diff --git a/recipes/crypto-createcipheriv-migration/tests/expected/esm-named-decipher.js b/recipes/crypto-createcipheriv-migration/tests/expected/esm-named-decipher.js new file mode 100644 index 00000000..e8fcb85f --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/expected/esm-named-decipher.js @@ -0,0 +1,10 @@ +import { createDecipheriv as createDecipher, scryptSync } from "node:crypto"; + +const decrypted = (() => { + // DEP0106: Replace the placeholders below with the salt and IV that were stored with the ciphertext. + const __dep0106Salt = /* TODO: stored salt Buffer */ Buffer.alloc(16); + const __dep0106Iv = /* TODO: stored IV Buffer */ Buffer.alloc(16); + const __dep0106Key = scryptSync("secret", __dep0106Salt, 32); + // DEP0106: Ensure __dep0106Salt and __dep0106Iv match the values used during encryption. + return createDecipher("aes-192-cbc", __dep0106Key, __dep0106Iv); +})(); diff --git a/recipes/crypto-createcipheriv-migration/tests/expected/esm-namespace.js b/recipes/crypto-createcipheriv-migration/tests/expected/esm-namespace.js new file mode 100644 index 00000000..50221891 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/expected/esm-namespace.js @@ -0,0 +1,10 @@ +import crypto from "node:crypto"; + +const encrypted = (() => { + const __dep0106Salt = crypto.randomBytes(16); + const __dep0106Key = crypto.scryptSync("pw", __dep0106Salt, 32); + const __dep0106Iv = crypto.randomBytes(16); + // DEP0106: Persist __dep0106Salt and __dep0106Iv with the ciphertext so it can be decrypted later. + // DEP0106: Adjust the derived key length (32 bytes) and IV length to match the chosen algorithm. + return crypto.createCipheriv("aes-256-cbc", __dep0106Key, __dep0106Iv); +})(); diff --git a/recipes/crypto-createcipheriv-migration/tests/input/commonjs-alias.js b/recipes/crypto-createcipheriv-migration/tests/input/commonjs-alias.js new file mode 100644 index 00000000..3e6a6fb1 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/input/commonjs-alias.js @@ -0,0 +1,5 @@ +const { createCipher: makeCipher } = require("node:crypto"); + +function wrap(password) { + return makeCipher("aes-192-cbc", password); +} diff --git a/recipes/crypto-createcipheriv-migration/tests/input/commonjs-decipher-destructured.js b/recipes/crypto-createcipheriv-migration/tests/input/commonjs-decipher-destructured.js new file mode 100644 index 00000000..ece9f3e3 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/input/commonjs-decipher-destructured.js @@ -0,0 +1,3 @@ +const { createDecipher } = require("node:crypto"); + +const decipher = createDecipher("aes-192-cbc", "secret"); diff --git a/recipes/crypto-createcipheriv-migration/tests/input/commonjs-decipher-namespace.js b/recipes/crypto-createcipheriv-migration/tests/input/commonjs-decipher-namespace.js new file mode 100644 index 00000000..abe937d7 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/input/commonjs-decipher-namespace.js @@ -0,0 +1,3 @@ +const crypto = require("crypto"); + +const decipher = crypto.createDecipher("aes-256-cbc", "pw"); diff --git a/recipes/crypto-createcipheriv-migration/tests/input/commonjs-destructured.js b/recipes/crypto-createcipheriv-migration/tests/input/commonjs-destructured.js new file mode 100644 index 00000000..aafc9e44 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/input/commonjs-destructured.js @@ -0,0 +1,3 @@ +const { createCipher } = require("node:crypto"); + +const cipher = createCipher("aes-128-cbc", "password123"); diff --git a/recipes/crypto-createcipheriv-migration/tests/input/commonjs-namespace.js b/recipes/crypto-createcipheriv-migration/tests/input/commonjs-namespace.js new file mode 100644 index 00000000..b4273c2e --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/input/commonjs-namespace.js @@ -0,0 +1,5 @@ +const crypto = require("node:crypto"); + +const algorithm = "aes-256-cbc"; +const password = "s3cret"; +const cipher = crypto.createCipher(algorithm, password); diff --git a/recipes/crypto-createcipheriv-migration/tests/input/commonjs-options.js b/recipes/crypto-createcipheriv-migration/tests/input/commonjs-options.js new file mode 100644 index 00000000..84838491 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/input/commonjs-options.js @@ -0,0 +1,3 @@ +const crypto = require("node:crypto"); + +const cipher = crypto.createCipher("aes-256-cbc", "pw", { authTagLength: 16 }); diff --git a/recipes/crypto-createcipheriv-migration/tests/input/esm-named-decipher.js b/recipes/crypto-createcipheriv-migration/tests/input/esm-named-decipher.js new file mode 100644 index 00000000..ea789113 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/input/esm-named-decipher.js @@ -0,0 +1,3 @@ +import { createDecipher } from "node:crypto"; + +const decrypted = createDecipher("aes-192-cbc", "secret"); diff --git a/recipes/crypto-createcipheriv-migration/tests/input/esm-namespace.js b/recipes/crypto-createcipheriv-migration/tests/input/esm-namespace.js new file mode 100644 index 00000000..d88a788d --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/tests/input/esm-namespace.js @@ -0,0 +1,3 @@ +import crypto from "node:crypto"; + +const encrypted = crypto.createCipher("aes-256-cbc", "pw"); diff --git a/recipes/crypto-createcipheriv-migration/workflow.yaml b/recipes/crypto-createcipheriv-migration/workflow.yaml new file mode 100644 index 00000000..5bbe7e92 --- /dev/null +++ b/recipes/crypto-createcipheriv-migration/workflow.yaml @@ -0,0 +1,25 @@ +# 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 + steps: + - name: Migrate `crypto.createCipher()`/`createDecipher()` to iv variants with secure key derivation. + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript From 95e81002240d489505899672408b7aad1312941e Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Tue, 4 Nov 2025 22:45:36 +0100 Subject: [PATCH 2/8] clean --- .../src/workflow.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/recipes/crypto-createcipheriv-migration/src/workflow.ts b/recipes/crypto-createcipheriv-migration/src/workflow.ts index 128e8b69..e0fade88 100644 --- a/recipes/crypto-createcipheriv-migration/src/workflow.ts +++ b/recipes/crypto-createcipheriv-migration/src/workflow.ts @@ -1,11 +1,12 @@ -import type { Edit, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; -import type Js from '@codemod.com/jssg-types/langs/javascript'; +import { EOL } from 'node:os'; import { getNodeImportCalls, 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'; +import type { Edit, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; +import type Js from '@codemod.com/jssg-types/langs/javascript'; type CallKind = 'cipher' | 'decipher'; @@ -158,13 +159,13 @@ function buildCipherReplacement(params: { const lines = [ '(() => {', - '\tconst __dep0106Salt = ' + randomBytesCall + '(16);', + `\tconst __dep0106Salt = ${randomBytesCall}(16);`, '\tconst __dep0106Key = ' + scryptCall + '(' + password + ', __dep0106Salt, 32);', - '\tconst __dep0106Iv = ' + randomBytesCall + '(16);', + `\tconst __dep0106Iv = ${randomBytesCall}(16);`, '\t// DEP0106: Persist __dep0106Salt and __dep0106Iv with the ciphertext so it can be decrypted later.', '\t// DEP0106: Adjust the derived key length (32 bytes) and IV length to match the chosen algorithm.', '\treturn ' + @@ -172,12 +173,12 @@ function buildCipherReplacement(params: { '(' + algorithm + ', __dep0106Key, __dep0106Iv' + - (options ? ', ' + options : '') + + (options ? `, ${options}` : '') + ');', '})()', ]; - return lines.join('\n'); + return lines.join(EOL); } function buildDecipherReplacement(params: { @@ -206,12 +207,12 @@ function buildDecipherReplacement(params: { '(' + algorithm + ', __dep0106Key, __dep0106Iv' + - (options ? ', ' + options : '') + + (options ? `, ${options}` : '') + ');', '})()', ]; - return lines.join('\n'); + return lines.join(EOL); } function getCallableBinding(binding: string, target: string): string { @@ -219,7 +220,7 @@ function getCallableBinding(binding: string, target: string): string { if (lastDot === -1) { return binding; } - return binding.slice(0, lastDot) + '.' + target; + return `${binding.slice(0, lastDot)}.${target}`; } function getMemberAccess(binding: string, member: string): string { @@ -227,7 +228,7 @@ function getMemberAccess(binding: string, member: string): string { if (lastDot === -1) { return member; } - return binding.slice(0, lastDot) + '.' + member; + return `${binding.slice(0, lastDot)}.${member}`; } function isDestructuredStatement(statement: SgNode): boolean { From 4b84f1267b8c291ecd99f76c91e4d78e10f402f7 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 20 Dec 2025 18:19:38 +0100 Subject: [PATCH 3/8] Update package-lock.json --- package-lock.json | 51 ++++++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1fd81912..93cc7ed7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1496,6 +1496,10 @@ "resolved": "recipes/createCredentials-to-createSecureContext", "link": true }, + "node_modules/@nodejs/crypto-createcipheriv-migration": { + "resolved": "recipes/crypto-createcipheriv-migration", + "link": true + }, "node_modules/@nodejs/crypto-fips-to-getFips": { "resolved": "recipes/crypto-fips-to-getFips", "link": true @@ -4279,7 +4283,7 @@ }, "recipes/buffer-atob-btoa": { "name": "@nodejs/buffer-atob-btoa", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4317,7 +4321,7 @@ }, "recipes/create-require-from-path": { "name": "@nodejs/create-require-from-path", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4328,7 +4332,7 @@ }, "recipes/createCredentials-to-createSecureContext": { "name": "@nodejs/createcredentials-to-createsecurecontext", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4337,9 +4341,20 @@ "@codemod.com/jssg-types": "^1.3.1" } }, + "recipes/crypto-createcipheriv-migration": { + "name": "@nodejs/crypto-createcipheriv-migration", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + } + }, "recipes/crypto-fips-to-getFips": { "name": "@nodejs/crypto-fips-to-getFips", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4372,7 +4387,7 @@ }, "recipes/fs-access-mode-constants": { "name": "@nodejs/fs-access-mode-constants", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4383,7 +4398,7 @@ }, "recipes/fs-truncate-fd-deprecation": { "name": "@nodejs/fs-truncate-fd-deprecation", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4394,7 +4409,7 @@ }, "recipes/http-classes-with-new": { "name": "@nodejs/http-classes-with-new", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4413,7 +4428,7 @@ }, "recipes/node-url-to-whatwg-url": { "name": "@nodejs/node-url-to-whatwg-url", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "devDependencies": { "@codemod.com/jssg-types": "^1.3.1" @@ -4421,7 +4436,7 @@ }, "recipes/process-assert-to-node-assert": { "name": "@nodejs/process-assert-to-node-assert", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4432,7 +4447,7 @@ }, "recipes/process-main-module": { "name": "@nodejs/process-main-module", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4443,7 +4458,7 @@ }, "recipes/repl-builtin-modules": { "name": "@nodejs/repl-builtin-modules", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4454,7 +4469,7 @@ }, "recipes/repl-classes-with-new": { "name": "@nodejs/repl-classes-with-new", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4465,7 +4480,7 @@ }, "recipes/rmdir": { "name": "@nodejs/rmdir", - "version": "1.1.0", + "version": "1.1.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4476,7 +4491,7 @@ }, "recipes/slow-buffer-to-buffer-alloc-unsafe-slow": { "name": "@nodejs/slow-buffer-to-buffer-alloc-unsafe-slow", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4487,7 +4502,7 @@ }, "recipes/tmpdir-to-tmpdir": { "name": "@nodejs/tmpdir-to-tmpdir", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4498,7 +4513,7 @@ }, "recipes/types-is-native-error": { "name": "@nodejs/types-is-native-error", - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@nodejs/codemod-utils": "*" }, @@ -4530,7 +4545,7 @@ }, "recipes/util-log-to-console-log": { "name": "@nodejs/util-log-to-console-log", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" @@ -4541,7 +4556,7 @@ }, "recipes/util-print-to-console-log": { "name": "@nodejs/util-print-to-console-log", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "@nodejs/codemod-utils": "*" From 4fb3589c9a2fb4e5979853fd3d33ba9102a21934 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 20 Dec 2025 18:29:35 +0100 Subject: [PATCH 4/8] WIP --- package-lock.json | 10 +++++----- recipes/crypto-createcipheriv-migration/package.json | 3 ++- .../crypto-createcipheriv-migration/src/workflow.ts | 5 +++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 93cc7ed7..31490e37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2381,10 +2381,9 @@ } }, "node_modules/dedent": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", - "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", - "dev": true, + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", "license": "MIT", "peerDependencies": { "babel-plugin-macros": "^3.1.0" @@ -4346,7 +4345,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@nodejs/codemod-utils": "*" + "@nodejs/codemod-utils": "*", + "dedent": "^1.7.1" }, "devDependencies": { "@codemod.com/jssg-types": "^1.0.9" diff --git a/recipes/crypto-createcipheriv-migration/package.json b/recipes/crypto-createcipheriv-migration/package.json index 2dc423b3..cb9e4c7b 100644 --- a/recipes/crypto-createcipheriv-migration/package.json +++ b/recipes/crypto-createcipheriv-migration/package.json @@ -19,6 +19,7 @@ "@codemod.com/jssg-types": "^1.0.9" }, "dependencies": { - "@nodejs/codemod-utils": "*" + "@nodejs/codemod-utils": "*", + "dedent": "^1.7.1" } } diff --git a/recipes/crypto-createcipheriv-migration/src/workflow.ts b/recipes/crypto-createcipheriv-migration/src/workflow.ts index e0fade88..f24e926e 100644 --- a/recipes/crypto-createcipheriv-migration/src/workflow.ts +++ b/recipes/crypto-createcipheriv-migration/src/workflow.ts @@ -1,4 +1,5 @@ import { EOL } from 'node:os'; +import dedent from 'dedent'; import { getNodeImportCalls, getNodeImportStatements, @@ -194,8 +195,8 @@ function buildDecipherReplacement(params: { const lines = [ '(() => {', '\t// DEP0106: Replace the placeholders below with the salt and IV that were stored with the ciphertext.', - '\tconst __dep0106Salt = /* TODO: stored salt Buffer */ Buffer.alloc(16);', - '\tconst __dep0106Iv = /* TODO: stored IV Buffer */ Buffer.alloc(16);', + "\tconst __dep0106Salt = /* TODO: stored salt Buffer */ Buffer.alloc(16);", + "\tconst __dep0106Iv = /* TODO: stored IV Buffer */ Buffer.alloc(16);", '\tconst __dep0106Key = ' + scryptCall + '(' + From 1555cd63d242bc25dd8ac2bd973296fdebfb1712 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sat, 20 Dec 2025 18:31:46 +0100 Subject: [PATCH 5/8] use dedent --- .../src/workflow.ts | 64 ++++++------------- 1 file changed, 20 insertions(+), 44 deletions(-) diff --git a/recipes/crypto-createcipheriv-migration/src/workflow.ts b/recipes/crypto-createcipheriv-migration/src/workflow.ts index f24e926e..9a469933 100644 --- a/recipes/crypto-createcipheriv-migration/src/workflow.ts +++ b/recipes/crypto-createcipheriv-migration/src/workflow.ts @@ -158,28 +158,16 @@ function buildCipherReplacement(params: { const scryptCall = getMemberAccess(binding, 'scryptSync'); const cipherCall = getCallableBinding(binding, 'createCipheriv'); - const lines = [ - '(() => {', - `\tconst __dep0106Salt = ${randomBytesCall}(16);`, - '\tconst __dep0106Key = ' + - scryptCall + - '(' + - password + - ', __dep0106Salt, 32);', - `\tconst __dep0106Iv = ${randomBytesCall}(16);`, - '\t// DEP0106: Persist __dep0106Salt and __dep0106Iv with the ciphertext so it can be decrypted later.', - '\t// DEP0106: Adjust the derived key length (32 bytes) and IV length to match the chosen algorithm.', - '\treturn ' + - cipherCall + - '(' + - algorithm + - ', __dep0106Key, __dep0106Iv' + - (options ? `, ${options}` : '') + - ');', - '})()', - ]; - - return lines.join(EOL); + return dedent(` + (() => { + const __dep0106Salt = ${randomBytesCall}(16); + const __dep0106Key = ${scryptCall}(${password}, __dep0106Salt, 32); + const __dep0106Iv = ${randomBytesCall}(16); + // DEP0106: Persist __dep0106Salt and __dep0106Iv with the ciphertext so it can be decrypted later. + // DEP0106: Adjust the derived key length (32 bytes) and IV length to match the chosen algorithm. + return ${cipherCall}(${algorithm}, __dep0106Key, __dep0106Iv${options ? `, ${options}` : ''}); + })() +`); } function buildDecipherReplacement(params: { @@ -192,28 +180,16 @@ function buildDecipherReplacement(params: { const scryptCall = getMemberAccess(binding, 'scryptSync'); const decipherCall = getCallableBinding(binding, 'createDecipheriv'); - const lines = [ - '(() => {', - '\t// DEP0106: Replace the placeholders below with the salt and IV that were stored with the ciphertext.', - "\tconst __dep0106Salt = /* TODO: stored salt Buffer */ Buffer.alloc(16);", - "\tconst __dep0106Iv = /* TODO: stored IV Buffer */ Buffer.alloc(16);", - '\tconst __dep0106Key = ' + - scryptCall + - '(' + - password + - ', __dep0106Salt, 32);', - '\t// DEP0106: Ensure __dep0106Salt and __dep0106Iv match the values used during encryption.', - '\treturn ' + - decipherCall + - '(' + - algorithm + - ', __dep0106Key, __dep0106Iv' + - (options ? `, ${options}` : '') + - ');', - '})()', - ]; - - return lines.join(EOL); + return dedent(` + (() => { + // DEP0106: Replace the placeholders below with the salt and IV that were stored with the ciphertext. + const __dep0106Salt = /* TODO: stored salt Buffer */ Buffer.alloc(16); + const __dep0106Iv = /* TODO: stored IV Buffer */ Buffer.alloc(16); + const __dep0106Key = ${scryptCall}(${password}, __dep0106Salt, 32); + // DEP0106: Ensure __dep0106Salt and __dep0106Iv match the values used during encryption. + return ${decipherCall}(${algorithm}, __dep0106Key, __dep0106Iv${options ? `, ${options}` : ''}); + })() +`); } function getCallableBinding(binding: string, target: string): string { From 4fb2e31ae276d980b0e537d9976f0abacd66e1a6 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sun, 21 Dec 2025 10:35:35 +0100 Subject: [PATCH 6/8] Update workflow.ts --- .../src/workflow.ts | 384 +++++++----------- 1 file changed, 142 insertions(+), 242 deletions(-) diff --git a/recipes/crypto-createcipheriv-migration/src/workflow.ts b/recipes/crypto-createcipheriv-migration/src/workflow.ts index 9a469933..8245b548 100644 --- a/recipes/crypto-createcipheriv-migration/src/workflow.ts +++ b/recipes/crypto-createcipheriv-migration/src/workflow.ts @@ -1,4 +1,3 @@ -import { EOL } from 'node:os'; import dedent from 'dedent'; import { getNodeImportCalls, @@ -11,23 +10,12 @@ import type Js from '@codemod.com/jssg-types/langs/javascript'; type CallKind = 'cipher' | 'decipher'; -type StatementChange = { - rename: Map; - additions: Set; -}; - -type BindingEntry = { - property: string; - local: string; -}; - type CollectParams = { rootNode: SgNode; statement: SgNode; binding: string; kind: CallKind; edits: Edit[]; - statementChanges: Map, StatementChange>; seenCallRanges: Set; }; @@ -38,43 +26,29 @@ type CollectParams = { export default function transform(root: SgRoot): string | null { const rootNode = root.root(); const edits: Edit[] = []; - const statementChanges = new Map, StatementChange>(); const seenCallRanges = new Set(); - for (const statement of collectCryptoStatements(root)) { - const cipherBinding = safeResolveBinding(statement, '$.createCipher'); - if (cipherBinding) { - collectCallEdits({ - rootNode, - statement, - binding: cipherBinding, - kind: 'cipher', - edits, - statementChanges, - seenCallRanges, - }); - } + const importStatements = [ + ...getNodeImportStatements(root, 'crypto'), + ...getNodeImportCalls(root, 'crypto'), + ...getNodeRequireCalls(root, 'crypto'), + ]; - const decipherBinding = safeResolveBinding(statement, '$.createDecipher'); - if (decipherBinding) { - collectCallEdits({ - rootNode, - statement, - binding: decipherBinding, - kind: 'decipher', - edits, - statementChanges, - seenCallRanges, - }); - } - } + if (!importStatements.length) return null; + + for (const statement of importStatements) { + const cipherBinding = resolveBindingPath(statement, '$.createCipher'); + collectCallEdits( + { rootNode, statement, binding: cipherBinding, kind: 'cipher', edits, seenCallRanges } + ); - for (const [statement, change] of statementChanges) { - const edit = applyStatementChanges(statement, change); - if (edit) edits.push(edit); + const decipherBinding = resolveBindingPath(statement, '$.createDecipher'); + collectCallEdits( + { rootNode, statement, binding: decipherBinding, kind: 'decipher', edits, seenCallRanges } + ); } - if (edits.length === 0) return null; + if (!edits.length) return null; return rootNode.commitEdits(edits); } @@ -85,9 +59,10 @@ function collectCallEdits({ binding, kind, edits, - statementChanges, seenCallRanges, }: CollectParams) { + if (!binding || binding === '') return; + const patterns = [ `${binding}($ALGORITHM, $PASSWORD, $OPTIONS)`, `${binding}($ALGORITHM, $PASSWORD)`, @@ -116,37 +91,48 @@ function collectCallEdits({ const optionsText = call.getMatch('OPTIONS')?.text()?.trim(); - const replacement = - kind === 'cipher' - ? buildCipherReplacement({ - binding, - algorithm, - password, - options: optionsText, - }) - : buildDecipherReplacement({ - binding, - algorithm, - password, - options: optionsText, - }); - - edits.push(call.replace(replacement)); - - if (isDestructuredStatement(statement)) { - const change = ensureStatementChange(statementChanges, statement); - // Ensure the binding points to the iv-based API - const sourceName = kind === 'cipher' ? 'createCipher' : 'createDecipher'; - const targetName = `${sourceName}iv`; - change.rename.set(sourceName, targetName); - if (kind === 'cipher') { - change.additions.add('randomBytes'); - } - change.additions.add('scryptSync'); + if (kind === 'cipher') { + const replacement = buildCipherReplacement({ + binding, + algorithm, + password, + options: optionsText, + }); + edits.push(call.replace(replacement)); + } else { + const replacement = buildDecipherReplacement({ + binding, + algorithm, + password, + options: optionsText, + }); + edits.push(call.replace(replacement)); + } + + // Update the corresponding import/require binding if present. + // Rename `createCipher`/`createDecipher` -> `createCipheriv`/`createDecipheriv` + // and add helper bindings (`scryptSync`, and `randomBytes` for cipher). + const sourceName = kind === 'cipher' ? 'createCipher' : 'createDecipher'; + const targetName = `${sourceName}iv`; + + const additions: string[] = kind === 'cipher' ? ['randomBytes', 'scryptSync'] : ['scryptSync']; + + // Preserve any local alias (e.g. `createCipher: makeCipher`) by + // constructing a property:local string for the renamed binding. + const local = findLocalSpecifierName(statement, sourceName); + + // Prefer an explicit update for destructured/named imports when + // present so we can preserve aliasing and ordering exactly. + const explicit = updateDestructuredStatement(statement, sourceName, targetName, local, additions); + + if (explicit) { + edits.push(explicit); } } } + + function buildCipherReplacement(params: { binding: string; algorithm: string; @@ -208,198 +194,112 @@ function getMemberAccess(binding: string, member: string): string { return `${binding.slice(0, lastDot)}.${member}`; } -function isDestructuredStatement(statement: SgNode): boolean { - return Boolean( - statement.find({ rule: { kind: 'object_pattern' } }) || - statement.find({ rule: { kind: 'named_imports' } }), - ); -} - -function ensureStatementChange( - statementChanges: Map, StatementChange>, +function updateDestructuredStatement( statement: SgNode, -): StatementChange { - let change = statementChanges.get(statement); - if (!change) { - change = { rename: new Map(), additions: new Set() }; - statementChanges.set(statement, change); - } - return change; -} - -function applyStatementChanges( - statement: SgNode, - change: StatementChange, + oldName: string, + targetName: string, + localName: string | undefined, + additions: string[], ): Edit | undefined { - if (change.rename.size === 0 && change.additions.size === 0) { - return undefined; - } - - if ( - statement.kind() === 'import_statement' || - statement.kind() === 'import_clause' - ) { - return updateImportSpecifiers(statement, change); + let namedImports = statement.find({ rule: { kind: 'named_imports' } }); + if (!namedImports) { + const clause = statement.find({ rule: { kind: 'import_clause' } }); + if (clause) namedImports = clause.find({ rule: { kind: 'named_imports' } }); } + if (namedImports) { + const isEsm = namedImports.parent()?.kind() === 'import_clause'; + + // Work on textual specifiers to preserve formatting and order. + const content = namedImports.text().replace(/^{\s*|\s*}$/g, ''); + const parts = content.split(',').map((p) => p.trim()).filter(Boolean); + const entries: string[] = parts.map((p) => { + if (new RegExp('^' + escapeRegExp(oldName) + '(\\b|\\s|:|\\s+as\\b)').test(p)) { + const local = localName ?? oldName; + return isEsm ? `${targetName} as ${local}` : `${targetName}: ${local}`; + } + return p; + }); - if (statement.find({ rule: { kind: 'object_pattern' } })) { - return updateRequirePattern(statement, change); + for (const a of additions) { + if (!entries.some((e) => new RegExp('\\b' + escapeRegExp(a) + '\\b').test(e))) entries.push(a); + } + return namedImports.replace(`{ ${entries.join(', ')} }`); } - return undefined; -} - -function updateImportSpecifiers( - statement: SgNode, - change: StatementChange, -): Edit | undefined { - const clause = - statement.kind() === 'import_clause' - ? statement - : statement.find({ rule: { kind: 'import_clause' } }); - if (!clause) return undefined; - - const namedImports = clause.find({ rule: { kind: 'named_imports' } }); - if (!namedImports) return undefined; - - const specNodes = namedImports.findAll({ - rule: { kind: 'import_specifier' }, - }); - if (specNodes.length === 0) return undefined; - - const entries: BindingEntry[] = specNodes.map((spec) => - parseImportSpecifier(spec.text()), - ); - let modified = false; - - for (const entry of entries) { - const newProperty = change.rename.get(entry.property); - if (newProperty && newProperty !== entry.property) { - entry.property = newProperty; - modified = true; + const objectPattern = statement.find({ rule: { kind: 'object_pattern' } }); + if (objectPattern) { + const pairs = objectPattern.findAll({ rule: { any: [{ kind: 'pair_pattern' }, { kind: 'shorthand_property_identifier_pattern' }] } }); + if (pairs.length === 0) return undefined; + + const entries: string[] = []; + for (const p of pairs) { + if (p.kind() === 'pair_pattern') { + const key = p.find({ rule: { kind: 'property_identifier' } }); + const value = p.children().find((c) => c.kind() === 'identifier'); + const prop = key.text(); + const local = value.text() ?? prop; + + const localToUse = localName ?? local; + entries.push(`${targetName}: ${localToUse}`); + } else { + const text = p.text(); + + if (text === oldName) { + const local = text; + const localToUse = localName ?? local; + entries.push(`${targetName}: ${localToUse}`); + } + } } - } - for (const addition of change.additions) { - const exists = entries.some( - (entry) => entry.property === addition || entry.local === addition, - ); - if (!exists) { - entries.push({ property: addition, local: addition }); - modified = true; + for (const a of additions) { + if (!entries.some((e) => e.includes(a))) entries.push(a); } + + return objectPattern.replace(`{ ${entries.join(', ')} }`); } - if (!modified) return undefined; + return undefined; +} - const rendered = entries - .map((entry) => - entry.property === entry.local - ? entry.property - : `${entry.property} as ${entry.local}`, - ) - .join(', '); +function escapeRegExp(str: string): string { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} - return namedImports.replace(`{ ${rendered} }`); +function getRangeKey(node: SgNode): string { + const range = node.range(); + return `${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}`; } -function updateRequirePattern( - statement: SgNode, - change: StatementChange, -): Edit | undefined { - const objectPattern = statement.find({ rule: { kind: 'object_pattern' } }); - if (!objectPattern) return undefined; +function findLocalSpecifierName(statement: SgNode, propertyName: string): string | undefined { + // pair_pattern: { prop: local } + const pairs = statement.findAll({ rule: { kind: 'pair_pattern' } }); + for (const pair of pairs) { + const key = pair.find({ rule: { kind: 'property_identifier' } }); - const specNodes = objectPattern.findAll({ - rule: { - any: [ - { kind: 'pair_pattern' }, - { kind: 'shorthand_property_identifier_pattern' }, - ], - }, - }); - if (specNodes.length === 0) return undefined; - - const entries: BindingEntry[] = specNodes.map((spec) => - parseRequireSpecifier(spec.text()), - ); - let modified = false; - - for (const entry of entries) { - const newProperty = change.rename.get(entry.property); - if (newProperty && newProperty !== entry.property) { - entry.property = newProperty; - modified = true; + if (key && key.text() === propertyName) { + const value = pair.children().find((c) => c.kind() === 'identifier'); + if (value) return value.text(); } } - for (const addition of change.additions) { - const exists = entries.some( - (entry) => entry.property === addition || entry.local === addition, - ); - if (!exists) { - entries.push({ property: addition, local: addition }); - modified = true; + // import_specifier: { name, alias } + const specs = statement.findAll({ rule: { kind: 'import_specifier' } }); + for (const s of specs) { + const nameNode = s.field && s.field('name'); + const aliasNode = s.field && s.field('alias'); + const idNode = s.find({ rule: { kind: 'identifier' } }); + const prop = (nameNode && nameNode.text()) || idNode?.text(); + + if (prop && prop === propertyName) { + if (aliasNode) return aliasNode.text(); + return prop; } } - if (!modified) return undefined; - - const rendered = entries - .map((entry) => - entry.property === entry.local - ? entry.property - : `${entry.property}: ${entry.local}`, - ) - .join(', '); + // shorthand destructure + const sh = statement.find({ rule: { kind: 'shorthand_property_identifier_pattern' } }); + if (sh && sh.text() === propertyName) return propertyName; - return objectPattern.replace(`{ ${rendered} }`); -} - -function parseImportSpecifier(text: string): BindingEntry { - const parts = text - .split(/\s+as\s+/) - .map((value) => value.trim()) - .filter(Boolean); - if (parts.length === 2) { - return { property: parts[0], local: parts[1] }; - } - const name = parts[0] ?? text.trim(); - return { property: name, local: name }; -} - -function parseRequireSpecifier(text: string): BindingEntry { - const parts = text - .split(':') - .map((value) => value.trim()) - .filter(Boolean); - if (parts.length === 2) { - return { property: parts[0], local: parts[1] }; - } - const name = parts[0] ?? text.trim(); - return { property: name, local: name }; -} - -function collectCryptoStatements(root: SgRoot): SgNode[] { - return [ - ...getNodeImportStatements(root, 'crypto'), - ...getNodeImportCalls(root, 'crypto'), - ...getNodeRequireCalls(root, 'crypto'), - ]; -} - -function safeResolveBinding( - node: SgNode, - path: string, -): string | undefined { - try { - return resolveBindingPath(node, path) ?? undefined; - } catch { - return undefined; - } -} - -function getRangeKey(node: SgNode): string { - const range = node.range(); - return `${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}`; + return undefined; } From 8db3dd253db3042ff1b22f50b4106689760ef4f9 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Sun, 21 Dec 2025 10:36:49 +0100 Subject: [PATCH 7/8] simplify --- .../crypto-createcipheriv-migration/src/workflow.ts | 10 +++++----- utils/src/ast-grep/update-binding.test.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/recipes/crypto-createcipheriv-migration/src/workflow.ts b/recipes/crypto-createcipheriv-migration/src/workflow.ts index 8245b548..034eefeb 100644 --- a/recipes/crypto-createcipheriv-migration/src/workflow.ts +++ b/recipes/crypto-createcipheriv-migration/src/workflow.ts @@ -213,7 +213,7 @@ function updateDestructuredStatement( const content = namedImports.text().replace(/^{\s*|\s*}$/g, ''); const parts = content.split(',').map((p) => p.trim()).filter(Boolean); const entries: string[] = parts.map((p) => { - if (new RegExp('^' + escapeRegExp(oldName) + '(\\b|\\s|:|\\s+as\\b)').test(p)) { + if (new RegExp(`^${escapeRegExp(oldName)}(\\b|\\s|:|\\s+as\\b)`).test(p)) { const local = localName ?? oldName; return isEsm ? `${targetName} as ${local}` : `${targetName}: ${local}`; } @@ -221,7 +221,7 @@ function updateDestructuredStatement( }); for (const a of additions) { - if (!entries.some((e) => new RegExp('\\b' + escapeRegExp(a) + '\\b').test(e))) entries.push(a); + if (!entries.some((e) => new RegExp(`\\b${escapeRegExp(a)}\\b`).test(e))) entries.push(a); } return namedImports.replace(`{ ${entries.join(', ')} }`); } @@ -286,10 +286,10 @@ function findLocalSpecifierName(statement: SgNode, propertyName: string): st // import_specifier: { name, alias } const specs = statement.findAll({ rule: { kind: 'import_specifier' } }); for (const s of specs) { - const nameNode = s.field && s.field('name'); - const aliasNode = s.field && s.field('alias'); + const nameNode = s.field?.('name'); + const aliasNode = s.field?.('alias'); const idNode = s.find({ rule: { kind: 'identifier' } }); - const prop = (nameNode && nameNode.text()) || idNode?.text(); + const prop = (nameNode?.text()) || idNode?.text(); if (prop && prop === propertyName) { if (aliasNode) return aliasNode.text(); diff --git a/utils/src/ast-grep/update-binding.test.ts b/utils/src/ast-grep/update-binding.test.ts index bb1e1b07..5fd5fef9 100644 --- a/utils/src/ast-grep/update-binding.test.ts +++ b/utils/src/ast-grep/update-binding.test.ts @@ -985,7 +985,7 @@ describe('update-binding', () => { assert.notEqual(change, undefined); assert.strictEqual(change?.lineToRemove, undefined); - const sourceCode = node.commitEdits([change!.edit!]); + const sourceCode = node.commitEdits([change.edit!]); assert.strictEqual( sourceCode, From ae6040b9535d558abe723a0bce4a71b13ad31fd9 Mon Sep 17 00:00:00 2001 From: Augustin Mauroy <97875033+AugustinMauroy@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:04:29 +0100 Subject: [PATCH 8/8] Update workflow.ts Co-Authored-By: Jacob Smith <3012099+JakobJingleheimer@users.noreply.github.com> --- .../src/workflow.ts | 163 ++++++++++-------- 1 file changed, 91 insertions(+), 72 deletions(-) diff --git a/recipes/crypto-createcipheriv-migration/src/workflow.ts b/recipes/crypto-createcipheriv-migration/src/workflow.ts index 034eefeb..caad4380 100644 --- a/recipes/crypto-createcipheriv-migration/src/workflow.ts +++ b/recipes/crypto-createcipheriv-migration/src/workflow.ts @@ -1,9 +1,5 @@ import dedent from 'dedent'; -import { - getNodeImportCalls, - getNodeImportStatements, -} from '@nodejs/codemod-utils/ast-grep/import-statement'; -import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call'; +import { getModuleDependencies } from '@nodejs/codemod-utils/ast-grep/module-dependencies'; import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path'; import type { Edit, SgNode, SgRoot } from '@codemod.com/jssg-types/main'; import type Js from '@codemod.com/jssg-types/langs/javascript'; @@ -28,24 +24,30 @@ export default function transform(root: SgRoot): string | null { const edits: Edit[] = []; const seenCallRanges = new Set(); - const importStatements = [ - ...getNodeImportStatements(root, 'crypto'), - ...getNodeImportCalls(root, 'crypto'), - ...getNodeRequireCalls(root, 'crypto'), - ]; + const importStatements = getModuleDependencies(root, 'crypto'); if (!importStatements.length) return null; for (const statement of importStatements) { const cipherBinding = resolveBindingPath(statement, '$.createCipher'); - collectCallEdits( - { rootNode, statement, binding: cipherBinding, kind: 'cipher', edits, seenCallRanges } - ); + collectCallEdits({ + rootNode, + statement, + binding: cipherBinding, + kind: 'cipher', + edits, + seenCallRanges, + }); const decipherBinding = resolveBindingPath(statement, '$.createDecipher'); - collectCallEdits( - { rootNode, statement, binding: decipherBinding, kind: 'decipher', edits, seenCallRanges } - ); + collectCallEdits({ + rootNode, + statement, + binding: decipherBinding, + kind: 'decipher', + edits, + seenCallRanges, + }); } if (!edits.length) return null; @@ -91,23 +93,16 @@ function collectCallEdits({ const optionsText = call.getMatch('OPTIONS')?.text()?.trim(); - if (kind === 'cipher') { - const replacement = buildCipherReplacement({ + const replacement = buildDeCipherReplacement( + { binding, algorithm, password, options: optionsText, - }); - edits.push(call.replace(replacement)); - } else { - const replacement = buildDecipherReplacement({ - binding, - algorithm, - password, - options: optionsText, - }); - edits.push(call.replace(replacement)); - } + }, + kind, + ); + edits.push(call.replace(replacement)); // Update the corresponding import/require binding if present. // Rename `createCipher`/`createDecipher` -> `createCipheriv`/`createDecipheriv` @@ -115,7 +110,8 @@ function collectCallEdits({ const sourceName = kind === 'cipher' ? 'createCipher' : 'createDecipher'; const targetName = `${sourceName}iv`; - const additions: string[] = kind === 'cipher' ? ['randomBytes', 'scryptSync'] : ['scryptSync']; + const additions: string[] = + kind === 'cipher' ? ['randomBytes', 'scryptSync'] : ['scryptSync']; // Preserve any local alias (e.g. `createCipher: makeCipher`) by // constructing a property:local string for the renamed binding. @@ -123,7 +119,13 @@ function collectCallEdits({ // Prefer an explicit update for destructured/named imports when // present so we can preserve aliasing and ordering exactly. - const explicit = updateDestructuredStatement(statement, sourceName, targetName, local, additions); + const explicit = updateDestructuredStatement( + statement, + sourceName, + targetName, + local, + additions, + ); if (explicit) { edits.push(explicit); @@ -131,40 +133,39 @@ function collectCallEdits({ } } - - -function buildCipherReplacement(params: { - binding: string; - algorithm: string; - password: string; - options?: string; -}): string { - const { binding, algorithm, password, options } = params; - const randomBytesCall = getMemberAccess(binding, 'randomBytes'); +function buildDeCipherReplacement( + { + binding, + algorithm, + password, + options, + }: { + binding: string; + algorithm: string; + password: string; + options?: string; + }, + kind: 'decipher' | 'cipher', +): string { const scryptCall = getMemberAccess(binding, 'scryptSync'); - const cipherCall = getCallableBinding(binding, 'createCipheriv'); - - return dedent(` - (() => { - const __dep0106Salt = ${randomBytesCall}(16); - const __dep0106Key = ${scryptCall}(${password}, __dep0106Salt, 32); - const __dep0106Iv = ${randomBytesCall}(16); - // DEP0106: Persist __dep0106Salt and __dep0106Iv with the ciphertext so it can be decrypted later. - // DEP0106: Adjust the derived key length (32 bytes) and IV length to match the chosen algorithm. - return ${cipherCall}(${algorithm}, __dep0106Key, __dep0106Iv${options ? `, ${options}` : ''}); - })() -`); -} - -function buildDecipherReplacement(params: { - binding: string; - algorithm: string; - password: string; - options?: string; -}): string { - const { binding, algorithm, password, options } = params; - const scryptCall = getMemberAccess(binding, 'scryptSync'); - const decipherCall = getCallableBinding(binding, 'createDecipheriv'); + const method = getCallableBinding( + binding, + kind === 'cipher' ? 'createCipheriv' : 'createDecipheriv', + ); + + if (kind === 'cipher') { + const randomBytesCall = getMemberAccess(binding, 'randomBytes'); + return dedent(` + (() => { + const __dep0106Salt = ${randomBytesCall}(16); + const __dep0106Key = ${scryptCall}(${password}, __dep0106Salt, 32); + const __dep0106Iv = ${randomBytesCall}(16); + // DEP0106: Persist __dep0106Salt and __dep0106Iv with the ciphertext so it can be decrypted later. + // DEP0106: Adjust the derived key length (32 bytes) and IV length to match the chosen algorithm. + return ${method}(${algorithm}, __dep0106Key, __dep0106Iv${options ? `, ${options}` : ''}); + })() + `); + } return dedent(` (() => { @@ -173,7 +174,7 @@ function buildDecipherReplacement(params: { const __dep0106Iv = /* TODO: stored IV Buffer */ Buffer.alloc(16); const __dep0106Key = ${scryptCall}(${password}, __dep0106Salt, 32); // DEP0106: Ensure __dep0106Salt and __dep0106Iv match the values used during encryption. - return ${decipherCall}(${algorithm}, __dep0106Key, __dep0106Iv${options ? `, ${options}` : ''}); + return ${method}(${algorithm}, __dep0106Key, __dep0106Iv${options ? `, ${options}` : ''}); })() `); } @@ -211,9 +212,14 @@ function updateDestructuredStatement( // Work on textual specifiers to preserve formatting and order. const content = namedImports.text().replace(/^{\s*|\s*}$/g, ''); - const parts = content.split(',').map((p) => p.trim()).filter(Boolean); + const parts = content + .split(',') + .map((p) => p.trim()) + .filter(Boolean); const entries: string[] = parts.map((p) => { - if (new RegExp(`^${escapeRegExp(oldName)}(\\b|\\s|:|\\s+as\\b)`).test(p)) { + if ( + new RegExp(`^${escapeRegExp(oldName)}(\\b|\\s|:|\\s+as\\b)`).test(p) + ) { const local = localName ?? oldName; return isEsm ? `${targetName} as ${local}` : `${targetName}: ${local}`; } @@ -221,14 +227,22 @@ function updateDestructuredStatement( }); for (const a of additions) { - if (!entries.some((e) => new RegExp(`\\b${escapeRegExp(a)}\\b`).test(e))) entries.push(a); + if (!entries.some((e) => new RegExp(`\\b${escapeRegExp(a)}\\b`).test(e))) + entries.push(a); } return namedImports.replace(`{ ${entries.join(', ')} }`); } const objectPattern = statement.find({ rule: { kind: 'object_pattern' } }); if (objectPattern) { - const pairs = objectPattern.findAll({ rule: { any: [{ kind: 'pair_pattern' }, { kind: 'shorthand_property_identifier_pattern' }] } }); + const pairs = objectPattern.findAll({ + rule: { + any: [ + { kind: 'pair_pattern' }, + { kind: 'shorthand_property_identifier_pattern' }, + ], + }, + }); if (pairs.length === 0) return undefined; const entries: string[] = []; @@ -271,7 +285,10 @@ function getRangeKey(node: SgNode): string { return `${range.start.line}:${range.start.column}-${range.end.line}:${range.end.column}`; } -function findLocalSpecifierName(statement: SgNode, propertyName: string): string | undefined { +function findLocalSpecifierName( + statement: SgNode, + propertyName: string, +): string | undefined { // pair_pattern: { prop: local } const pairs = statement.findAll({ rule: { kind: 'pair_pattern' } }); for (const pair of pairs) { @@ -289,7 +306,7 @@ function findLocalSpecifierName(statement: SgNode, propertyName: string): st const nameNode = s.field?.('name'); const aliasNode = s.field?.('alias'); const idNode = s.find({ rule: { kind: 'identifier' } }); - const prop = (nameNode?.text()) || idNode?.text(); + const prop = nameNode?.text() || idNode?.text(); if (prop && prop === propertyName) { if (aliasNode) return aliasNode.text(); @@ -298,7 +315,9 @@ function findLocalSpecifierName(statement: SgNode, propertyName: string): st } // shorthand destructure - const sh = statement.find({ rule: { kind: 'shorthand_property_identifier_pattern' } }); + const sh = statement.find({ + rule: { kind: 'shorthand_property_identifier_pattern' }, + }); if (sh && sh.text() === propertyName) return propertyName; return undefined;