WinZip-AES-256 Interoperability
@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.
@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:
| Constraint | Behavior |
|---|---|
| AES-256 only | AES-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 Zip64 | Entries 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:
| Tool | Installed 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,exportCapabilitygrants).as-*family — the full@noy-db/as-zipAPI (toBytes,download,write,fromBytes) alongside its sibling exporters, including the encrypted-bundle alternative@noy-db/as-noydb.