mathmain@1.0.0 is an npm package that copies mathjs, with one extra call appended to the end of lusolve(). It passes the lower triangular matrix from an LU decomposition into a function named removeSolveValidation(), whose return value is assigned to an unused variable. That function is a decryption loader — and the password is derived from the caller’s own input, so there is no secret to find in the source.

The encryption layout is deliberate:

  • scrypt turns the password into a 256-bit key, AES-GCM decrypts the blob
  • The password is JSON.stringify(L), the lower factor of the coefficient matrix — a 3×3 Pascal matrix produces [[1,0,0],[1,1,0],[1,0.5,1]]
  • That password decrypts the first-stage filename to graph.js, which the loader writes next to the original and require()s
  • SafeDep’s own brute force (16,922 then 533 candidate passwords) found nothing; JFrog recovered the trigger matrix first

The loader is inert until the exact right input arrives, which is the attacker’s best defence. A normal import never reaches the added code, wrong passwords fail the GCM authentication check before any file is written, and install hooks are absent — so anything that merely installs and runs the package sees clean behaviour.

What it decrypts is a small remote access implant: host reconnaissance, an X25519 keypair, detached Node child processes, a bundled copy of ethers 5.7.2, and a Slack agent that polls conversations.history every ten seconds, reassembles a chunked file transfer, writes subwatcher and starts it. Telegram and a Sepolia smart contract provide backup command channels. The same loader ships in mathsbase and math-universe, in versions that do not appear in the public GitHub repos those packages link to.

Two takeaways generalise past this campaign. Published artefacts are now the attack surface — scan the tarball, not the repo, and diff the two. And version selection matters: the default release of mathsbase was clean while the previous one carried the implant, so anything that only inspects the latest version misses it. SafeDep is careful about the limits here too: a hash match does not prove the code ran, and they never found a public caller that supplies the trigger.