Kzip — Format Spec (v1 — restic-compatible bootstrap)
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
- Modificar qualquer estrutura encoded em arquivos .kz/.kzip
- Adicionar ou remover campos de metadata serializados
All triggers
- Modificar qualquer estrutura encoded em arquivos .kz/.kzip
- Alterar o layout de pack-files, snapshots, índices ou árvores no repositório kzip
- Adicionar ou remover campos de metadata serializados
- Trocar algoritmos de compressão default ou cripto AEAD
- Definir novo magic number, frame extension, chunk type, ou block format
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:
restic design.rst v0.18.1— comportamento do repositóriorestic references/design.rst v0.18.1— formato wire-level
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 emconfig). - 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.Typevalores 3-255 reservados (BCJ-pre-filter blob, signature blob, etc.).Snapshot.tagsaceita 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); version0x01. dShard + pShard ≤ 256(klauspost/reedsolomon constraint).parity=pShardshards ofceil(dataSize / dShard)bytes.- Generated by
kzip recovery encode <file>; consumed byverify/repair. - Out-of-band: removing all
.kzrsfiles 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:
- 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)
- Lifecycle:
--migratecommand 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
engines/compress/kzip/docs/rfcs/RFC-001-charter.mdengines/compress/kzip/docs/upstream/restic-NOTICE.mdhttps://github.com/restic/restic/blob/v0.18.1/doc/design.rsthttps://github.com/restic/restic/blob/v0.18.1/doc/references/design.rst