stable

WinZip-AES-256 Interoperability

Which third-party archive tools can open password-protected archives produced by @noy-db/as-zip, and which third-party archives its reader accepts.

@noy-db/as-zip exports a collection's records and attachments from a noy-db vault (an encrypted document store — see the as-* family overview) as a single .zip file. When you pass a password, every entry in that archive is encrypted with WinZip-AES-256 — a widely implemented extension to the ZIP format — so the recipient can open it with an ordinary archive tool instead of needing noy-db installed. This page states which third-party tools @noy-db/as-zip's automated interop test suite currently exercises, in both directions: archives noy-db writes being opened by other tools, and archives other tools write being read back by noy-db.

WinZip-AES is the interop layer — a portable handoff to standard archive tools, protected by a single shared secret. For multi-recipient, revocable, audited egress of noy-db state, use the encrypted-bundle exporter @noy-db/as-noydb instead.

What the format guarantees

Two vendor versions of WinZip-AES exist, and both appear in the wild:

  • AE-1 — the entry's CRC-32 checksum is stored alongside the ciphertext.
  • AE-2 — the CRC-32 field is zeroed; integrity is covered by the encryption's own authentication code instead.

@noy-db/as-zip writes AE-2 and reads both AE-1 and AE-2. Each encrypted entry carries a 16-byte random salt, a 2-byte password verifier (fails fast on a wrong secret), the AES-CTR ciphertext, and a 10-byte HMAC-SHA1 authentication code.

Deliberate scope limits, enforced at both write and read time:

ConstraintBehavior
AES-256 onlyAES-128/192 and legacy ZipCrypto archives are refused with ZipCipherError
STORE only (no compression)DEFLATE/bzip2/lzma entries are refused with ZipReadError
Single-disk, no Zip64Entries larger than 4 GiB are not supported

Opening a noy-db archive in other tools

@noy-db/as-zip ships an automated interop suite that writes a password-protected archive with this package and then extracts it with a third-party tool, byte-for-byte, across four payload shapes: a 1-byte entry, a 16-byte entry (exactly one AES block), a non-ASCII entry path, and a 1 MiB payload. It runs against whichever of these tools is on the machine's PATH:

ToolInstalled on, in the release-time check
7-Zip (7z / 7zz)Linux, macOS, Windows
unar (The Unarchiver)macOS only

That release-time check runs the suite on Linux, macOS, and Windows runners as an advisory step — it reports cross-tool regressions but does not block a release, so treat it as a point-in-time result rather than a standing guarantee. You can reproduce it yourself with the relevant tool installed:

cd packages/as-zip
pnpm vitest run __tests__/interop.test.ts

Archive Utility (macOS's built-in unzip) and WinRAR both implement the WinZip-AES extension and are expected to accept these archives, but neither is exercised by the automated suite.

Producing an encrypted archive takes one extra option:

import { toBytes } from '@noy-db/as-zip'

// vault: an unlocked noy-db Vault whose keyring grants
// exportCapability { plaintext: ['zip'] } to the caller.
const archive = await toBytes(vault, {
  records: { collection: 'invoices' },
  password: 'shared-with-recipient-2026',
})
// → Uint8Array — a standard single-disk .zip; WinZip-AES-aware
//   tools prompt for the password on extract

The recipient extracts it with their tool of choice:

7z x -p'shared-with-recipient-2026' invoices.zip
unar -p 'shared-with-recipient-2026' invoices.zip
# Archive Utility / Finder: double-click, enter the password when prompted

Reading third-party archives back into noy-db

The reverse direction is exercised the same way, with 7-Zip as the writer: the suite creates an archive with 7-Zip using AES-256 and the STORE method, then reads it back with @noy-db/as-zip's own reader, on the same Linux/macOS/Windows runners described above. Two things to know when producing such an archive:

  • 7-Zip writes AE-1 by default. That is fine — the reader accepts both vendor versions.
  • 7-Zip compresses with DEFLATE by default. The reader is STORE-only, so force stored (uncompressed) entries when creating the archive:
7z a -tzip -mm=Copy -mem=AES256 -p'shared-with-recipient-2026' data.zip records.json

Reading it back:

import { fromBytes } from '@noy-db/as-zip'

const plan = await fromBytes(vault, archiveBytes, {
  collection: 'invoices',
  password: 'shared-with-recipient-2026',
})

// Nothing is written yet. plan.plan is a VaultDiff — every record that
// would be added, modified, or deleted to bring the vault to match the
// archive, computed against the caller's own current state:
console.log(plan.plan.summary)
// → { add: 2, modify: 1, delete: 0, total: 3 }
{
  "added": [
    { "collection": "invoices", "id": "inv-104", "record": { "id": "inv-104", "amount": 4200 } }
  ],
  "modified": [
    { "collection": "invoices", "id": "inv-101", "fieldsChanged": ["amount"] }
  ],
  "deleted": []
}

plan.policy defaults to 'merge': added and modified records are written, deleted records are left alone (pass { policy: 'replace' } to also delete, or { policy: 'insert-only' } to skip modifications). Nothing is committed until apply() runs, and the whole batch is transactional — if any write in the plan fails, none of it is kept:

await plan.apply()
// → Promise<void> — added + modified records committed in one transaction

A wrong password, or ciphertext that was tampered with, surfaces as ZipCipherError; structural problems (truncated file, unsupported compression method, missing AES metadata) surface as ZipReadError:

import { ZipCipherError, ZipReadError } from '@noy-db/as-zip'

await fromBytes(vault, archiveBytes, { collection: 'invoices', password: 'wrong' })
// → throws ZipCipherError (code 'ZIP_CIPHER_ERROR')

See also

  • as-* family overview — the export authorisation model (capability tiers, exportCapability grants).
  • as-* family — the full @noy-db/as-zip API (toBytes, download, write, fromBytes) alongside its sibling exporters, including the encrypted-bundle alternative @noy-db/as-noydb.