npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tr-data-encrypt

v1.1.0

Published

Symmetric file encryption in the TRDE container format: streamed AES-256-GCM with optional compression, atomic and in-place file operations, keys as raw bytes or JWK. The file codec of tr-data-escrow, extracted into its own package.

Readme

tr-data-encrypt

Symmetric file encryption in the TRDE container format: streamed AES-256-GCM with optional compression, atomic and in-place file operations, keys as raw bytes or JWK. Zero dependencies, Node.js 18 or newer, TypeScript types included.

Typical use: store a file encrypted at rest and stream it back as plaintext, or encrypt a file in place and decrypt it in place later.

Provenance

This package did not appear from nowhere. It is the file codec of tr-data-escrow, extracted into its own package so that it can be used without the escrow:

  • The codec and the container format date from tr-data-escrow 1.0.0 (2026-07-02) and have been unchanged since. The container magic bytes, TRDE, come from that origin, and the format is deliberately frozen: every file ever written by tr-data-escrow decrypts with this package.
  • The code was extracted from tr-data-escrow 4.0.3 (commit abf0948, 2026-09-08) with generic function names and a few additions (in-place operation, header inspection, raw keys); see CHANGELOG.md for the exact mapping.
  • The format is specified in full, with verified test vectors, in ENCRYPTED-FILE-FORMAT.md, which moved here with the code. The specification is the contract; the test suite checks the package against it and against an independent implementation written from the document alone.
  • Same author and licence as tr-data-escrow. It belongs to the same family of packages (tr-jwk, tr-jwt, tr-jwe, tr-kmac, tr-key-vault-client, tr-data-escrow) and is maintained and released in step with tr-data-escrow, which is to depend on it from its next major version.

Install

npm install tr-data-encrypt

Requires Node.js >= 18. No runtime dependencies.

Quick start

const { generateKey, encryptFile, decryptFile, decryptToStream } = require('tr-data-encrypt');

// A fresh 32-byte AES-256 key as an "oct" JWK. Keep it somewhere safe.
const key = generateKey();

// Encrypt a file in place, compressing it first. The plaintext is replaced
// by the container in one atomic rename once the container is complete.
await encryptFile('/data/report.pdf', key, { compression: 'gzip' });

// Stream the plaintext back without touching the file.
const stream = await decryptToStream('/data/report.pdf', key);
stream.pipe(response);   // see "Streaming caveat" below

// Or decrypt it in place again.
await decryptFile('/data/report.pdf', key);

Everything that takes a source accepts a file path, a Buffer, or a Readable. Everything that takes a key accepts 32 raw bytes or a JWK.

Entry points

| Import | Contents | |---------------------------|-------------------------------------------------| | tr-data-encrypt | everything | | tr-data-encrypt/encrypt | the encrypt functions, key generation, compression validation | | tr-data-encrypt/decrypt | the decrypt functions, the range readers, and readHeader |

The two subpaths load no code of the other side, so a program that only writes containers never loads decryption code, and vice versa.

The container

A file is one AES-256-GCM message with a 20-byte cleartext header that is authenticated as GCM additional data:

magic "TRDE" (4) | version 0x01 | enc 0x01 | comp | ivlen 0x0C | iv (12)
ciphertext … | tag (16)

comp names the compression applied before encryption (0 none, 1 deflate, 2 gzip, 3 brotli; 4 is reserved for zstd). There is no length field, no filename, no timestamp, and no key material in the container. The key is delivered out of band, and each container is meant to have its own fresh key. The full specification, with test vectors, is ENCRYPTED-FILE-FORMAT.md.

API

Keys

  • generateKey(): OctJwk — a fresh random 32-byte key as { kty: 'oct', k, alg: 'A256GCM', key_ops: ['encrypt', 'decrypt'], use: 'enc', kid } with a random UUID kid. This is the member set tr-data-escrow embeds per file.
  • keyBytes(key: KeyInput): Buffer — the 32 raw bytes of a key. KeyInput is a Uint8Array of exactly 32 bytes, or a JWK object whose k is the base64url encoding of 32 bytes; a JWK with a kty other than "oct" is rejected. Throws TypeError otherwise. Every function below applies the same rule to its key argument.

Encrypt (tr-data-encrypt/encrypt)

Options: { compression?: 'none' | 'deflate' | 'gzip' | 'brotli' }, default 'none'. 'zstd' is a reserved container code and is rejected.

  • encryptToStream(source, key, options?): Readable — returns a stream emitting the complete container. A fresh random IV is used. A failure in any stage destroys the stream with that error; destroying the stream tears the pipeline down. Argument errors throw synchronously before anything is opened.
  • encryptToBuffer(source, key, options?): Promise<Buffer> — the whole container in memory.
  • encryptToFile(source, key, destination, options?): Promise<void> — writes the container atomically: into a temporary file beside the destination, fsynced, then renamed into place once the whole source has been consumed. On failure the temporary file is removed and nothing appears at the destination. An existing destination is replaced atomically.
  • encryptFile(path, key, options?): Promise<void> — encrypts the file at path in place, or to options.destination when given. In place means the plaintext file is replaced by the container in one rename; at no point is a partial or mixed file visible at the path. The plaintext's former disk blocks are not erased.

Decrypt (tr-data-encrypt/decrypt)

  • decryptToStream(source, key): Promise<Readable> — resolves once the header has been read and validated (bad magic, version, encryption or compression code, or truncation inside the header reject here) to a stream of the content. See the streaming caveat.
  • decryptToBuffer(source, key): Promise<Buffer> — the whole content, verified before it is returned.
  • decryptToFile(source, key, destination): Promise<void> — writes the content atomically as above; the file appears only after the GCM tag and the decompression have been verified.
  • decryptFile(path, key, options?): Promise<void> — decrypts the container at path in place, or to options.destination. A failure leaves the container untouched.
  • readHeader(source): Promise<ContainerHeader> — the cleartext header: { version, enc, compressionCode, compression, iv }, where compression is the name for the code or null for an unassigned code. No key is needed and nothing is verified: only the header syntax is checked. A Readable source is consumed and destroyed.

Random access (tr-data-encrypt/decrypt)

  • decryptRangeToStream(path, key, options?): Promise<Readable> and decryptRangeToBuffer(path, key, options?): Promise<Buffer> — a byte range of the content of the container file at path, read from just that part of the file. Options { start?, end? } are content-byte offsets, both inclusive, as in fs.createReadStream: start defaults to 0 and end to the last byte (null or undefined mean the default). Offsets must be non-negative integers; there is no end-relative addressing. start past the end yields an empty result, end past the end is clamped, and start greater than end rejects with RangeError. Only an uncompressed container with a 12-byte IV qualifies; anything else rejects before a stream exists, as do a bad header, a truncated file, or a missing file.

    The output is never verified. The GCM tag covers the whole ciphertext and cannot be checked against a part of it, so these functions do not check it at all, even when no range is given. A modified byte in the file yields a modified byte in the output and nothing detects it. Use them for content whose integrity is established otherwise — for example a file verified once with decryptToFile or decryptToBuffer and served from trusted storage after that — and use decryptToStream when integrity matters. For an uncompressed container the content length is the file size minus 36, which is what an HTTP Content-Range header needs.

Streaming caveat

AES-GCM verifies its tag at the very end. A decryptToStream consumer therefore receives content before it is verified; a tag or decompression failure arrives as an 'error' event on the stream, and everything received so far must be discarded. When that is not acceptable, use decryptToBuffer or decryptToFile, which deliver nothing until verification has succeeded.

Types and constants

CompressionName, DataSource (string | Buffer | Readable), KeyInput, OctJwk, ContainerHeader, EncryptOptions, EncryptFileOptions, DecryptFileOptions; validateCompression(value) (returns the name, null for undefined/null, throws TypeError otherwise), compressionName(code), COMPRESSION_CODES; and the format constants MAGIC, FORMAT_VERSION, ENC_AES256GCM, IV_LENGTH, TAG_LENGTH, KEY_LENGTH, FIXED_HEADER_LENGTH.

Errors

Argument problems throw or reject with TypeError. A malformed container rejects with an Error whose message begins TRDE container: (bad magic, unsupported version or encryption code, unknown or reserved compression code, zero IV length, truncated). A wrong key, a tampered container, or a corrupt compressed payload surfaces as the underlying node:crypto or node:zlib error. Filesystem errors propagate as such.

Compression

Content may be passed through a node:zlib stream before encryption: deflate is the zlib format (RFC 1950, not raw DEFLATE), gzip is RFC 1952 with one member, brotli is RFC 7932. The compression is recorded in the container header and needs no parameter on the reading side. Compressing before encrypting reveals the compressed size rather than the content size; if size is sensitive, avoid compression or pad the content.

Security notes

  • Use a fresh key for every container. With a random 12-byte IV and a fresh key per container, nonce reuse cannot occur. Reusing a key across containers makes IV uniqueness the caller's responsibility.
  • The header is authenticated but visible: it reveals the compression in use, and the ciphertext length reveals the payload length exactly.
  • The container does not bind its own filename or any external identity. If a container must be tied to a name or record, the surrounding system has to do it (tr-data-escrow does so through its manifest).
  • Bound the output when decrypting untrusted containers: a small container can decompress to a very large content.
  • The key is as sensitive as the content. Do not log it.

Testing

npm test

Builds dist first (the entry-point tests run against the built package), then runs vitest: the specification's conformance test, the codec tests, the range readers, entry-point separation, and fixtures written by the published tr-data-escrow 4.0.3.

License

MIT, © 2026 Timo J. Rinne.