Kzip — Format Spec (v1 — restic-compatible bootstrap)

kzip specs/kzip/format.kmd

Formato canônico de arquivos `.kz` (single-file) e `.kzip` (multi-file archive) gerados por `kzip`, o compactador da Koder Stack. Durante o bootstrap (v1), o formato é compatível byte-a-byte com o repositório restic v0.18.x — single source of truth. Divergências futuras requerem bump de versão major + ticket explícito + nota de incompatibilidade.

When this spec applies

Primary triggers

All triggers

Specification body

Kzip Format Specification — v1 (restic-compatible)

0. Stage and stability

Esta v1 do formato é restic-compatible durante o bootstrap. Documentos canônicos de referência:

A spec abaixo resume o contrato; em caso de conflito, os documentos restic acima prevalecem (até a v1 ser ratificada).

1. File extensions

Ext Descrição
.kz Single-file mode (futuro) — frame de stream comprimido análogo a .zst. Reservado mas não emitido pelo bootstrap v0.1.
.kzip Multi-file mode (atual) — repositório completo num diretório (não num único arquivo). Quando empacotado para transporte, agregado num .tar opcional.

Nota: restic não usa extensão pra repositórios. Adotamos .kzip (sufixo de diretório ou tar) para signal in-name.

2. Repository layout

Um repositório kzip é um diretório com a seguinte estrutura. Todos os arquivos abaixo são encrypted com a chave do repositório (exceto config que tem header não-cifrado pra detection).

<repo-root>/
├── config                    ← repository config (JSON, encrypted body)
├── keys/<id>                 ← chaves derivadas + Argon2 KDF
├── data/<2-hex>/<pack-id>    ← packs (blobs concatenados + comprimidos)
├── index/<index-id>          ← índices map (blob-id → pack-id+offset+length)
├── snapshots/<snap-id>       ← snapshot metadata (timestamp, paths, tree-id)
├── locks/<lock-id>           ← exclusion locks (TTL ~30 min)
└── HEAD                      ← (opcional) ponteiro para snapshot mais recente

Todos os IDs são SHA-256 hashes em hex lowercase (64 chars).

3. Crypto

3.1 Master key derivation

  • KDF: Argon2id (default time=3, memory=64MiB, parallelism=1).
  • Salt: 64 bytes random per-key.
  • Output: 64 bytes (32 encryption + 32 MAC).

3.2 Encryption

  • Cipher: AES-256-CTR.
  • IV: 16 bytes random per-blob.
  • MAC: Poly1305 (32 bytes) over (IV || ciphertext).
  • AEAD construction: encrypt-then-MAC.

3.3 Format wire de cada blob criptografado

+-----------------------------+----------------+-------------------------+
| IV (16 bytes random)       | ciphertext (N) | Poly1305 tag (32 bytes) |
+-----------------------------+----------------+-------------------------+

Total = IV(16) + N + tag(32). N pode ser 0 (empty plaintext válido).

3.4 Chave de repositório

Cada keys/<id> contém JSON cifrado com a master key, com:

{
  "created": "<RFC3339>",
  "username": "<string>",
  "hostname": "<string>",
  "kdf": "argon2id",
  "n": 524288, "r": 1, "p": 1,    // Argon2 params
  "salt": "<base64>",
  "data": "<base64-encrypted-master-key>"
}

A senha do operador deriva uma key-encryption-key via Argon2id; essa KEK descriptografa data para obter a master key real do repositório. Múltiplas keys/ podem coexistir (multi-user / rotation).

4. Pack files

Pack files agregam vários blobs num único arquivo para amortizar overhead I/O e melhorar compressão.

4.1 Layout

+---------------+---------------+-----+---------------+-------------------+
| blob 1        | blob 2        | ... | blob N        | header (encrypted) |
+---------------+---------------+-----+---------------+-------------------+
                                                       ↑
                                                  ends at EOF - 4
+----+
| H  | header length (uint32 LE, last 4 bytes of file)
+----+

4.2 Pack header (após decrypt)

type PackHeaderEntry struct {
    Type   byte    // 0=data, 1=tree, 2=padding (legacy)
    Length uint32  // length of plaintext
    ID     [32]byte // SHA-256 of plaintext
}

Header inteiro = repeated PackHeaderEntry + cifrado AEAD.

4.3 Tipos de blob

Type Conteúdo Geração
data (0) Chunk de arquivo (bytes brutos do arquivo após chunking) Pelo backup, antes de cifrar
tree (1) JSON serializado de uma árvore de diretório Pelo backup, ao subir cada dir

Ambos são comprimidos antes de cifrar (algoritmo configurável; default zstd nível 3 em v1).

5. Content-defined chunking (CDC)

  • Algoritmo: Rabin fingerprint sobre janela rolante.
  • Polinomial: random per-repo (gerado no init, salvo em config).
  • Tamanhos: min=512 KiB, max=8 MiB, target=1 MiB (defaults restic — podem ser tunáveis em RFC futura).
  • Boundary: hash mod 2²⁰ == 0 (ajustável para hit target).

Cada chunk vira um data blob (após dedup pelo hash).

6. Trees

Um tree blob é JSON serializado:

{
  "nodes": [
    {
      "name": "filename",
      "type": "file" | "dir" | "symlink" | "fifo" | "socket" | "blockdev" | "chardev",
      "mode": "0644",
      "mtime": "<RFC3339>",
      "atime": "<RFC3339>",
      "ctime": "<RFC3339>",
      "uid": 1000, "gid": 1000,
      "user": "koder", "group": "koder",
      "size": 12345,
      "content": ["<blob-id>", "<blob-id>"],         // for files
      "subtree": "<tree-id>",                         // for dirs
      "linktarget": "<path>",                         // for symlinks
      "extended_attributes": [{"name":"...","value":"<base64>"}]
    }
  ]
}

xattrs e ACLs são preservados via extended_attributes. Hard-links não são deduplicados explicitamente — mesma content array implica mesmo conteúdo, mas inode identity não é preservada.

7. Snapshots

Cada snapshots/<id> contém JSON cifrado:

{
  "time": "<RFC3339>",
  "tree": "<root-tree-id>",
  "paths": ["/home/user/docs"],
  "hostname": "host",
  "username": "user",
  "uid": 1000, "gid": 1000,
  "tags": ["weekly", "automated"],
  "parent": "<previous-snapshot-id>",   // optional
  "program_version": "kzip 0.1.0-bootstrap (restic-fork)"
}

8. Indices

index/<id> é JSON cifrado mapping cada blob-id para sua localização:

{
  "supersedes": ["<old-index-id>"],
  "packs": [
    {
      "id": "<pack-id>",
      "blobs": [
        {
          "id": "<blob-id>",
          "type": "data" | "tree",
          "offset": 0,
          "length": 4194304,
          "uncompressed_length": 5242880   // optional, for compressed blobs
        }
      ]
    }
  ]
}

prune consolida múltiplos índices num só (substituindo via supersedes).

9. Locks

Exclusion locks em locks/<id>:

{
  "time": "<RFC3339>",
  "exclusive": true | false,
  "hostname": "host",
  "username": "user",
  "pid": 12345
}

TTL ~30 min; locks abandonados expiram. Stale locks detectados via PID liveness.

10. Compression

Blobs (data + tree) são comprimidos antes de cifrar. v1 suporta:

Algorithm Default Notas
zstd level 3 sim balance perf/ratio default
zstd level 1 opt-in máxima velocidade
zstd level 11 opt-in (--compression max) máxima compressão
nenhum opt-in (--compression off) escapa quando dados já comprimidos

LZMA, BWT, BCJ filters não suportados em v1 (planejados em ticket #003).

11. Magic numbers / detection

  • Pack files: sem magic number explícito — detection via tentativa de decrypt do header lido pelos últimos 4 bytes (length).
  • Config: JSON cifrado com header "version": 2 (após decrypt).
  • Repository version atual: 2 (mesmo do restic v0.18.x).

12. Endianness

Todos campos numéricos binários são little-endian.

13. Future extension hooks

A v1 reserva os seguintes campos para uso futuro sem quebrar compat:

  • PackHeaderEntry.Type valores 3-255 reservados (BCJ-pre-filter blob, signature blob, etc.).
  • Snapshot.tags aceita arbitrary strings para metadata Koder-specific (koder:repo=hub, koder:role=daily-backup).
  • Config JSON aceita campos não-reconhecidos sem erro (forward-compat) — kzip futuro pode adicionar signing_key_id, recovery_records_enabled, bcj_filter_chain, etc.

13.1 Sidecar artifacts (out-of-band, not part of repo format)

Some kzip features write sidecar files alongside repo artifacts without modifying the repo format. Sidecars are additive: a v1 reader/restic that doesn't recognize the sidecar simply ignores it.

.kzrs — Reed-Solomon parity sidecar (kzip ticket #007 v1 sidecar mode):

Layout (big-endian where applicable):

+-----+--------+--------+--------+----------+----------+----------+
| 4B  |  1B    |  1B    |  1B    |   4B BE  |   32B    |  N×B     |
| KZRS| ver=01 | dShard | pShard | dataSize | sha256   |  parity  |
+-----+--------+--------+--------+----------+----------+----------+
  • Magic KZRS (0x4B 0x5A 0x52 0x53); version 0x01.
  • dShard + pShard ≤ 256 (klauspost/reedsolomon constraint).
  • parity = pShard shards of ceil(dataSize / dShard) bytes.
  • Generated by kzip recovery encode <file>; consumed by verify/repair.
  • Out-of-band: removing all .kzrs files leaves the repo intact and readable by stock restic.

The pack-format-embedded variant (kzip ticket #009 — planned) will move parity into a new PackHeaderEntry.Type=4 blob with per-shard checksums; the sidecar form will continue to be supported in parallel for files outside the repo (e.g. raw deploy artifacts).

14. Divergence policy

Mudanças que quebram a compat byte-a-byte com restic v0.18.x:

  1. Exigem RFC novo (e.g. kzip-RFC-002-format-divergence.md) com:
    • Justificativa (feature impossível com formato atual)
    • Caminho de migração (forward-compat se possível)
    • Bump de repository.version (3 ou superior)
  2. Lifecycle:
    • --migrate command para converter repos v2 → vN
    • Suporte de leitura para v2 mantido por ≥1 ano após bump
    • Snapshot nota explícita: kzip 0.X.0 introduced repo v3, see CHANGELOG

Mudanças que mantêm byte-compat (não quebram):

  • Adicionar campos JSON novos (forward-compat por convention)
  • Adicionar tags Koder-specific
  • Adicionar comprehensible algorithms (zstd higher levels, etc.)

Estes não exigem RFC, apenas atualização desta spec + entrada CHANGELOG.

15. Testing

  • Tests de regressão em engines/compress/kzip/tests/regression/ devem incluir golden-hash compare contra binaries restic v0.18.x para garantir interop.
  • Tests em engines/compress/kzip/engine/restic_vendor/ (test suite upstream) preservados como-is.

Anexo A — Mapeamento kzip ↔ restic

Durante o bootstrap, todos os termos restic são equivalentes aos termos kzip. Mapeamento:

Restic Kzip Notas
restic init kzip init mesmo behavior
repository repository (.kzip) extension para signal
pack file pack file layout idêntico
blob blob idêntico
snapshot snapshot idêntico
Argon2id KDF Argon2id KDF idêntico
AES-256-CTR + Poly1305 AES-256-CTR + Poly1305 idêntico
Rabin chunker Rabin chunker idêntico

Quando começar a divergir (ticket #003 BCJ filters, etc.), entradas serão adicionadas a este anexo com data de divergência.

References