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

filecoin-encryption-envelope

v0.1.0

Published

Filecoin Encryption Envelope: COSE envelope encryption

Readme

Filecoin Encryption Envelope

Filecoin Encryption Envelope (FEE) is a proposed format for portable encrypted data, currently under review as a draft FRC in filecoin-project/FIPs#1294. An encoded FEE object starts with a COSE (CBOR) envelope containing the algorithm, nonce information, and optional recipient records, followed by the detached ciphertext. Separating that metadata from the ciphertext is meant to help different applications and key-management systems work with the same encrypted data stored on Filecoin.

This package implements a draft FEE profile. It supports chunked AES-256-GCM encryption, authenticated range reads, and one-shot AES-GCM for small objects. It uses Web Crypto and Web Streams in Node.js and browsers.

The Go implementation, go-fee, supports scheme 2, A256KW and ECDH-ES+A256KW (X25519) recipients, and range decryption. It does not implement scheme 1. The two implementations are checked against cross-implementation test vectors.

Install

npm install filecoin-encryption-envelope

For a small object, the whole-object API is the shortest way to get started:

import * as fee from 'filecoin-encryption-envelope'

const cek = crypto.getRandomValues(new Uint8Array(fee.constants.KEY_SIZE))
const plaintext = new TextEncoder().encode('Hello, Filecoin')
const encoded = await fee.aesGcm.encrypt(plaintext, { cek })
const restored = await fee.aesGcm.decrypt(encoded, cek)
console.log(new TextDecoder().decode(restored)) // Hello, Filecoin

Keep the CEK secret and store it through your key-management system. You need that same key to decrypt the object.

The root encrypt() and decrypt() functions use the chunked scheme (scheme 2). Use them for files and for objects you may need to read by range. The separate aesGcm functions use whole-object encryption (scheme 1) and accept at most 64 MiB of plaintext. The library does not switch between schemes automatically.

Encrypt and decrypt a stream

The application supplies a 32-byte content-encryption key (CEK). Generate a fresh, secret CEK for each new encryption unless your key-management system defines a different reuse policy. The library generates the encryption nonce.

import * as fee from 'filecoin-encryption-envelope'

const cek = crypto.getRandomValues(new Uint8Array(fee.constants.KEY_SIZE))

// plaintextSource and encryptedSource are ReadableStream<Uint8Array> values.
// encryptedSink and plaintextSink are WritableStream<Uint8Array> values.
await plaintextSource.pipeThrough(fee.encrypt({ cek })).pipeTo(encryptedSink)
await encryptedSource.pipeThrough(fee.decrypt(cek)).pipeTo(plaintextSink)

Decryption releases each plaintext chunk only after its tag verifies. The final chunk is written when the encryption input closes, so a source that never closes cannot produce a complete object.

If the exact plaintext length is known, pass contentLength. The library commits to that length in the protected header and fails the stream if the source supplies a different number of bytes. Omit it when the length cannot be guaranteed.

For a browser File and a file handle supplied by the application:

async function saveEncryptedFile(file: File, handle: FileSystemFileHandle, cek: Uint8Array) {
  const destination = await handle.createWritable()
  await file.stream()
    .pipeThrough(fee.encrypt({ cek, contentLength: file.size }))
    .pipeTo(destination)
}

If a stream errors, discard all output already written or read. A storage sink that keeps a partial upload must remove it; earlier chunks are not a complete encrypted object.

Recipients

A recipient record wraps the CEK so another holder of a key-encryption key (KEK) can recover it. The built-in recipient algorithm is A256KW: both sides must already have the same secret 32-byte KEK. A kid identifies which KEK to try; it is not itself a secret.

const cek = crypto.getRandomValues(new Uint8Array(fee.constants.KEY_SIZE))
const kid = new TextEncoder().encode('recipient-1')
// kek is a secret 32-byte key supplied by your key-management system.

await plaintextSource
  .pipeThrough(fee.encrypt({
    cek,
    recipients: [{ alg: fee.constants.ALG_A256KW, kek, kid }],
  }))
  .pipeTo(encryptedSink)

const unwrapper = await fee.recipients.createA256KWUnwrapper([{ kek, kid }])
await encryptedSource.pipeThrough(fee.decryptWith(unwrapper)).pipeTo(plaintextSink)

The same CEK encrypts the content once; recipient records contain wrapped copies of that CEK. A caller that already has the CEK can use decrypt(cek) without unwrapping a recipient.

Read a byte range

Range reads work only with the chunked scheme. A RandomAccessSource reports the exact encoded-object size, including the envelope, and opens byte ranges from one immutable object version. An HTTP adapter can use an immutable URL or enforce an object version with conditional requests. The size and every range response must refer to that same version.

This adapter assumes versionedUrl identifies an immutable object and size is its exact encoded length. The library checks that the returned stream has exactly the requested number of bytes; the adapter also checks the HTTP range response before handing it over.

function httpSource(versionedUrl: string, size: number): fee.RandomAccessSource {
  return {
    size,
    async openRange(offset, length) {
      const end = offset + length - 1
      const response = await fetch(versionedUrl, {
        headers: { Range: `bytes=${offset}-${end}` },
      })
      if (
        response.status !== 206 ||
        response.headers.get('Content-Range') !== `bytes ${offset}-${end}/${size}` ||
        !response.body
      ) {
        await response.body?.cancel()
        throw new Error('Unexpected HTTP range response')
      }
      return response.body
    },
  }
}

const source = httpSource(versionedUrl, exactEncodedLength)
const info = await fee.parse(source)
if (info.scheme !== 'chunked') throw new Error('Range reads require a chunked object')

const result = await fee.decryptRange(
  source,
  cek,
  { offset: 100_000, length: 50_000 },
  { params: info.params }
)
await result.stream.pipeTo(rangeSink)

parse() reads the envelope without a key and returns parameters that let the range read skip a second envelope fetch. Its output is not authenticated. Use the returned params only with the same object version. A range read authenticates the chunks it fetches, not unread chunks elsewhere in the object.

Resolve a key from envelope metadata

A streaming reader can ask your application to derive or fetch the CEK after it sees the envelope. The resolver receives unauthenticated metadata, so validate any scope or identifier before using it for a key lookup.

// allowedScope and keyStore are supplied by your application.
const resolver: fee.KeyResolver = async (info) => {
  const scope = info.appMetadata?.scope
  const salt = info.appMetadata?.salt
  if (scope !== allowedScope || typeof salt !== 'string' || !/^[0-9a-f]{32}$/.test(salt)) {
    throw new Error('Envelope metadata is not allowed for this key lookup')
  }
  // keyStore is your application or key-management library.
  return keyStore.deriveCek({ scope, salt })
}

await encryptedSource.pipeThrough(fee.decrypt(resolver)).pipeTo(plaintextSink)

The resolver runs at most once, before any chunk is decrypted. Its checks control which key may be requested; they do not authenticate the metadata. For a range read, call parse(source), validate its metadata, derive the CEK, then pass that key and info.params to decryptRange().

Security and limits

  • CEKs and A256KW KEKs must be 32-byte Uint8Array values and cannot be all zero. The library validates their shape but cannot tell whether they are secret, unpredictable, or fresh.
  • Streaming options, including keys and metadata, are borrowed until the stream closes or errors. Do not modify or clear them while it is running. A plaintext block may be reused once its write() resolves. For a range read, the CEK is borrowed until the decryptRange() promise settles, and the source until the result stream closes or errors.
  • Application metadata is visible without a key. Its values are authenticated only after a content tag verifies. Content tags do not authenticate the recipient list or establish sender identity.
  • A failed stream may already have emitted bytes. Discard them all. Successful decryption of an entire object requires reading through the final chunk.

The chunked scheme permits an encoded object of at most 64 GiB. Scheme 1 permits at most 64 MiB of plaintext. For the wire profile, range rules, and other limits, see the implementation guide.