Skip to content

Package Format V14

Elman Mohammadi edited this page Oct 6, 2026 · 1 revision

Package Format V14

V14 packages use the canonical format identifier:

SECURE-PKG-V14

Portable packages use the .spkg14 extension.

Package layout

A V14 package directory has exactly this high-level structure:

package/
├── header.json
├── manifest.enc
├── complete.json
└── blobs/
    ├── <48-hex>.bin
    ├── <48-hex>.bin
    └── ...

Unknown top-level entries are rejected by the V14 package verifier.

header.json

The header contains:

  • format
  • version
  • schema
  • package_id
  • created_at
  • salt
  • crypto
  • header_binding
  • key_slots

The current V14 values are:

{
  "format": "SECURE-PKG-V14",
  "version": 14,
  "schema": 1
}

Package ID

The package ID is 24 random bytes represented as 48 lowercase hexadecimal characters.

Timestamp

The creation timestamp must match the exact UTC format:

YYYY-MM-DDTHH:MM:SSZ

It is parsed as a real UTC timestamp rather than being accepted only by a regular expression.

Cryptographic profile

The active profile contains:

{
  "kdf": {
    "name": "argon2id13",
    "salt_bytes": 32,
    "ops": "implementation-defined V14 moderate profile",
    "mem": "implementation-defined V14 moderate profile",
    "key_bytes": 32
  },
  "aead": "xchacha20poly1305-ietf",
  "key_wrap": "xchacha20poly1305-ietf",
  "stream": "secretstream-xchacha20poly1305",
  "key_separation": "HKDF-SHA-256",
  "hash": "sha256"
}

The implementation does not simply trust a claimed profile. The descriptor compares the supplied profile against the runtime's canonical V14 profile.

Header binding

The header core excludes the binding itself and the key slots. It is canonicalized with deterministic JSON ordering and hashed with SHA-256.

header_binding = SHA-256(CanonicalJSON(header_core))

The binding is then carried into authenticated data for key wrapping, manifest encryption, filename encryption, and file encryption.

Key slots

The header always contains the primary slot and contains a recovery slot that is either an authenticated slot or null.

A slot contains:

{
  "type": "primary | recovery",
  "version": 1,
  "nonce": "base64",
  "wrapped_key": "base64"
}

The wrapped value contains an authenticated encryption result for the 32-byte package root key.

manifest.enc

The manifest is canonical JSON encrypted with the dedicated manifest key.

Its authenticated plaintext structure contains:

  • format
  • version
  • schema
  • package ID
  • header binding
  • file count
  • directory count
  • node graph
  • total plaintext size
  • Merkle root

Manifest node model

Each node represents either a directory or a file.

Directory node:

{
  "id": "32 lowercase hex characters",
  "parent": "32 lowercase hex characters or null",
  "type": "dir",
  "name": "base64-encoded AEAD ciphertext"
}

File node:

{
  "id": "32 lowercase hex characters",
  "parent": "32 lowercase hex characters or null",
  "type": "file",
  "name": "base64-encoded AEAD ciphertext",
  "blob": "48-hex.bin",
  "size": 123,
  "cipher_hash": "64 lowercase hex characters"
}

The parent must refer to a directory node, and IDs must be unique.

complete.json

The completion record contains:

  • format
  • version
  • package ID
  • header binding
  • SHA-256 of the encrypted manifest
  • creation timestamp

This record provides an additional consistency check linking the final package state to the header and encrypted manifest.

Blob naming

Each encrypted file receives a random 24-byte blob ID represented by 48 lowercase hexadecimal characters, followed by .bin.

The blob name does not directly reveal the source filename.

V14 format-level limits

The descriptor currently defines these boundaries:

Limit Value
Header 64 KiB
Encrypted manifest plaintext ceiling 64 MiB
Nodes 100,000
Single plaintext file 16 GiB
Total plaintext 100 GiB
Blobs 100,000
Filename bytes 4 KiB
Hierarchy depth 256

These are format-level parser and resource-safety limits. Application-level transport limits can be different and are documented separately in Archive and Upload Security and Deployment.

Outer portable ZIP

The package directory is transported as a ZIP archive using .spkg14.

The ZIP container is treated as untrusted input and is therefore validated independently of the V14 package parser. The archive layer checks entry names, duplicates, unsafe paths, file/directory conflicts, entry counts, and actual streamed extraction sizes.

Why the format is directory-backed internally

A directory-backed structure makes the authenticated records easy to inspect and test independently while still allowing one portable ZIP object for transport.

The server removes the temporary unpacked V14 package after successful packaging so the canonical stored object is the portable .spkg14 archive rather than two copies of the encrypted data.

Clone this wiki locally