npm.io
0.2.0 • Published 2d ago

@pksuite/crypto

Licence
MIT
Version
0.2.0
Deps
3
Size
304 kB
Vulns
0
Weekly
0

@pksuite/crypto

Browser and Node cryptographic primitives shared by the pk suite. Version 0.1.0 provides canonical age/X25519 identities and the streaming PKFILE1 file envelope. It has no workflow, storage-provider, or network concepts. The ESM browser entry is intended for an application bundler. Its declared @noble/* dependencies remain external in both ESM and CommonJS artifacts so consumer lockfile updates change the code that actually executes.

import {
  createRecipientIdentity,
  createFileEncryption,
  disposeEnvelope,
  encryptChunk,
  finalizeEnvelope,
  openFileEnvelope,
  decryptChunk,
} from "@pksuite/crypto";

const recipient = createRecipientIdentity();
const encryption = createFileEncryption({
  recipient: recipient.recipient,
  plaintextSize: file.size,
  metadata: {
    name: file.name,
    mediaType: file.type || "application/octet-stream",
    lastModified: file.lastModified,
  },
});

for (let index = 0; index < encryption.chunkCount; index++) {
  const start = index * encryption.chunkSize;
  const plaintext = new Uint8Array(await file.slice(start, start + encryption.chunkSize).arrayBuffer());
  const ciphertext = encryptChunk({ state: encryption, index, plaintext });
  // Persist or upload this independently authenticated chunk before reading
  // the next slice. Retrying an index produces a fresh XChaCha nonce.
  await storeChunk(index, ciphertext);
}
finalizeEnvelope(encryption);

const decryption = openFileEnvelope({ identity: recipient.identity, header: encryption.header });
for (let index = 0; index < decryption.chunkCount; index++) {
  const plaintext = decryptChunk({ state: decryption, index, ciphertext: await loadChunk(index) });
  await consumePlaintext(plaintext);
}
finalizeEnvelope(decryption);

When the identity scalar is held elsewhere (e.g. an agent process that never releases it), pass unwrapFileKey instead of identity: the callback receives a copy of the envelope's age header and returns the 16-byte file key; the package copies the result, so the callback may wipe its own buffer.

finalizeEnvelope best-effort overwrites the file key retained by its state and makes that state unusable. Call disposeEnvelope(state) on cancellation or any abandoned upload/decryption. JavaScript cannot guarantee erasure of engine or cryptographic-library internal copies.

Metadata names are encrypted portable filename components, not paths. A consumer that writes files must still join the name beneath an explicitly chosen directory and must not decode or reinterpret it as path syntax.

The encrypted header exposes file size, chunk geometry, recipient count, and header length. Filename, media type, and modification time are encrypted. See dist/SPEC-FILE.md in the package, or spec/SPEC-FILE.md in the repository, for the implementation-independent wire format and threat contract.

vectors/file-v1.json contains fixed identity, header, metadata, nonce, AAD, and chunk outputs. An independent Node reference reader opens that frozen vector in the test suite. Every structured case in vectors/file-v1-tampering.json is executed by the conformance tests. The frozen age-cli-v1.age artifact was written by the external age CLI.

The mobile release gate is recorded under test/mobile/reports/ for Android Chrome and iOS Safari. Both devices completed foreground and background-resume runs with matching byte counts. Safari does not expose performance.memory; the harness records that measurement as unavailable.