Skip to content

crypto.createHmac(alg, key) returns "" when alg is a const reference (string-literal-only dispatch — same pattern as #1074) #1076

@proggeramlug

Description

@proggeramlug

Repro (12 lines, no imports beyond `crypto`)

```ts
import * as crypto from "crypto";

const key = "secret";
const data = "payload";

console.log("(1) bare literal: ", crypto.createHmac("sha256", key).update(data).digest("hex"));

for (const alg of ["sha256"]) {
console.log("(2) for-of binding: ", crypto.createHmac(alg, key).update(data).digest("hex"));
}

const alg = "sha256";
console.log("(3) const reference: ", crypto.createHmac(alg, key).update(data).digest("hex"));
```

Observed (perry 0.5.1008)

```
(1) bare literal: b82fcb791acec57859b989b430a826488ce2e479fdf92326bd0a2e8375a42ba4
(2) for-of binding: ← empty string
(3) const reference: ← empty string
```

Expected (tsx / Node)

All three print the same 64-char hex digest.

Pattern

Same shape as #1074 (jwt.sign `algorithm` option). The codegen for `crypto.createHmac` lowers based on whether the first argument is an inline `Expr::String` — `Expr::Identifier` (const refs), for-loop bindings, and any non-literal expression silently fall through to a path that produces an empty hash.

I'd guess `createHash`, `createCipheriv`, `createDecipheriv`, `createSign`, `createVerify` all share the bug (haven't tested all). Anywhere the runtime dispatches via a name string from the first arg.

Why this is a quiet trap

The chain runs without throwing — `.update(data).digest("hex")` is valid on whatever the broken `createHmac` returned — so callers see `""` back and only notice when a downstream comparison fails (e.g., HMAC-based webhook signature verification returns `false` 100% of the time, or TOTP verification never matches).

How this surfaces in shop-admin

`server/auth/totp.ts::computeTotp`:

```ts
const computeTotp = (secret: Buffer, counter: number): string => {
...
const hex = crypto.createHmac("sha1", secret).update(buf).digest("hex"); // inline literal — works
...
};
```

This call happens to use a literal so it works. PERRY_GAPS.md item B mis-diagnosed the smoke-test failure as a cross-module Buffer issue, but I just confirmed cross-module Buffer round-trips work fine; the real symptom would be: any nearby code that loops over algorithms (e.g., `for (const a of HASH_ALGS) { check(crypto.createHmac(a, k)...) }`) silently produces empties.

The Shopware/Woo webhook verifiers use `crypto.createHmac("sha256", secret).update(body).digest(...)` with inline literals, so they're not bitten — but anyone writing `const a = "sha256"; crypto.createHmac(a, secret)` is.

Suggested fix

Same as #1074: resolve simple compile-time-known const references before the literal-string match. `const a = "sha256"` is statically known. A small symbol-table lookup in the `createHmac` lowering would catch the common case.

Or: add a runtime path that takes the alg string at runtime and dispatches dynamically (slower but correct). Either is better than silent `""`.

Local checkout: perry 0.5.1008.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions