A CCM decipher skipped its one update() when the data was empty, and
final() accepted when an update had run without failing, so it never
compared the tag. setAuthTag() with any tag of the right length, then
update() with empty data, then final(), succeeded: anyone could forge an
empty message, or one with different AAD or a different declared
plaintextLength, without the key.
A decipher's update() now runs the authenticated decryption for empty
data too. As in OpenSSL, the tag covers the nonce, the AAD and the
length, so the right tag still opens an empty message and any other
tag, AAD or length fails in final() with "Unsupported state or unable to
authenticate data". final() succeeds only after a tag comparison has
succeeded. A cipher's empty update() is unchanged.
getCipherInfo(nameOrNid[, options]) runs in a new runtime unit,
scr_cipher_info.c, over a table that scripts/gen-cipher-info.mjs generates
from Node 24.22's getCipherInfo. The table covers every cipher OpenSSL
names, including the ones without a native implementation. Lookups match
names case-insensitively and accept OpenSSL's nids. The unit applies
cipher.js's argument checks and GetCipherInfo's keyLength and ivLength
rules, and returns the info object in Node's key order. CipherInfo maps
to a checked-dynamic value, so TypeScript and JavaScript code receive
Node's object. Reading the nid Node leaves out for the SIV and SM4 GCM,
CCM and XTS ciphers, where the declared type gives number, throws a
TypeError.
createCipheriv and createDecipheriv support aes-128/192/256-ccm (and the
id-aesN-ccm names) over OpenSSL's ccm128 steps. They follow Node's tag and
IV lengths, the plaintextLength requirement, the message-length caps and
the one-update rule, and report a failed authentication from final().
They also support the RFC 3394 and RFC 5649 key wrap ciphers (aes-N-wrap,
aesN-wrap, id-aesN-wrap, the -pad forms and the -inv ciphers) over
wrap128.c's routines, with Node's lengths, integrity failures and
one-update rule.
The new names follow the existing ones in the cipher table, so the lookup
cost of the existing ciphers is unchanged.
Cipheriv and Decipheriv values are Transforms, as in Node: they can be
written and ended, piped into, used as pipeline() and stream/promises
pipeline stages, composed, read, iterated with for await, read by
stream/consumers, passed to finished(), and listened to, and their stream
properties read as in Node. Each chunk goes through the cipher's
update(), and the end pushes final() or reports its error through
'error'; writing after final() throws out of write(). The stream unit
builds a cipher's Transform on its first stream use from operations the
cipher's handle provides (ScrDynHandleOps.transform), so programs that
use ciphers without streams link as before. The cipher is a
collector-headered object behind a traced handle: its Transform's steps
hold it, and the pair is reclaimed as a cycle. Through untyped values, in
a program that uses streams, a cipher's stream properties and its on,
once, off, pipe, unpipe, read, pause, resume, setEncoding, write, end,
destroy, cork and uncork members run on its Transform; other stream
members throw a coded SC2020 error.
A Transform holds a write's callback while its readable side is at the
high-water mark and runs it at the next read, and runs it on a tick once
the readable side has ended, as Transform._write does. write() on a
PassThrough, a Transform, a Hash or a cipher used to return true however
much the readable side buffered. zlib streams applied the rule already
and keep their timing.
pipeline() accepts function stages anywhere among its streams, so runs of
async generator stages can sit on both sides of a stream. A stage can be
named by a function declaration, or a const bound to a function
expression, that nothing reassigns; its body lowers again at the
pipeline with its source typed by the pipeline. The source can also be
an array, a string, a synchronous generator object, or a synchronous
generator function without parameters: an async generator steps the
source's iterator and yields each element, and the next stage receives
each element on the same tick as with Node's for await over the source.
The runtime's function-stage entry points take a bitmask of the function
positions and one (next, read, return) helper triple for each generator
pumped into a stream, so RUNTIME_ABI_VERSION is now 25. In JavaScript,
the chunks a stage receives from the previous stage's generator read as
the strings or Buffers it yields, so their string and Buffer methods
compile.
Corpus 7055-7059 cover cipher streams in TypeScript and JavaScript, their
errors, close order and backpressure, the pipeline stages and sources
with their tick order, and Transform backpressure.
The error identity cache (scr_dyn_from_error) keeps one checked-dynamic
node per error so a crossing error compares reference-equal and keeps the
properties set on it. It retained both sides for the whole process, so
every error that crossed a checked-dynamic boundary (a caught error read
through a cast, an error delivered to an untyped callback, a dns failure, a
warning) stayed alive until exit: 100,000 caught AssertionErrors read
through a cast grew the resident set by about 210 MiB.
The cache now sweeps itself. An entry whose error and node are referenced
only by the cache is unreachable (a node holds no reference to its error;
a registered subclass box holds one, and a spawnSync error's `error`
property refers to its own node), so a sweep releases both sides. Sweeps
run when the cache doubles past the entries the last sweep kept, so they
cost amortized constant time per crossing. The cache is per thread, and
objects shared across threads are published (immortal), so a sweep never
races another thread's reference. Live errors and live nodes keep their
entries: identity, rethrowing the same instance, isNativeError and kind
checks are unchanged.
scr_error_set_prop sets an own property on an error's node; dns, assert,
cipher, crypto, util.callbackify and warning errors use it for the
properties they attach. util.inspect of a runtime error renders through
its node: Node's own properties follow the bracket form in key order (a
system error's errno, code, syscall and path, and the extra properties a
throw site attached), and an AssertionError renders at depth 0 with long
string actual/expected values shortened, as its custom inspect does. A
node that contains itself renders as Node's <ref *1>/[Circular *1].
child_process errors carry Node's own properties:
- execSync and execFileSync: status, signal, output, pid, stdout and
stderr (Buffers, or strings with an encoding option); a spawn failure
or timeout adds errno, syscall, path, spawnargs and error first.
- execFile's callback and the promisified execFile/exec: code (the exit
status, or null after a signal), killed, signal and cmd, with stdout and
stderr when promisified; a spawn failure has errno, code, syscall, path,
spawnargs and cmd.
The sync forms echo the captured stderr before a timeout throw too, as
Node's inheritStderr does. The cp.execSync libCall takes a flag for Buffer
outputs.
An error's own `stack` stays non-enumerable when assigned through its
node. A worker's record-source table is freed when the worker exits.
createCipheriv, createDecipheriv, update(), setAAD() and setAuthTag()
now read the value a typed-ref box holds when it is a string, view,
ArrayBuffer or KeyObject. ERR_INVALID_ARG_TYPE names the class of a
class instance that arrives through an untyped value, as Node does,
instead of Object. An unknown output encoding that is not a string is
rendered as Node's %s formats it (util.inspect at depth 0, or the
object's own toString); a function, or an instance of a class that
defines toString, throws a coded SC2020 error. Cipher programs link the
inspect unit for that rendering.
5250 passes each argument through `any`, including class instances,
records and arrays that Node rejects, and 5254 covers output encodings
that are not strings from JavaScript.
- Every mbedTLS call in the cipher runtime is checked. A failed update(),
final() or setAAD() returns no output and retires the context, so later
calls throw and no tag is returned or accepted. An update past the
AES-GCM limit of 2^36 - 32 bytes per message throws Node's "Trying to
add data in unsupported state" error; it used to return zeroed output
and leave the tag covering only the earlier data.
- chacha20-poly1305 decryption without setAuthTag() fails in final()
again. Node 24.22 skips the tag check when authTagLength is not passed
either and returns unverified plaintext; limitations.mdx records the
difference.
- A runtime unit test moves contexts next to the GCM message and AAD
limits and checks that the failure is permanent; errors.test.ts covers
decryption without a tag.
- chacha20-poly1305 refuses non-empty AAD once data has been processed
(ERR_CRYPTO_INVALID_STATE); empty AAD is still accepted.
- chacha20-poly1305 decryption without setAuthTag() succeeds when no
authTagLength was passed, and is verified when one was.
- Changing the output encoding of update() or final() throws
ERR_INVALID_ARG_VALUE instead of ERR_INTERNAL_ASSERTION.
- The ctsMode and xtsStandard options are validated in Node's order;
the implemented ciphers report ERR_CRYPTO_UNSUPPORTED_OPERATION.
- The list of known but unimplemented cipher names follows Node 24.22's
getCiphers(), so the CBC-HMAC names are unknown ciphers.