Developer guide · Zikaron

Files and formats

The kinds of file the app and the command line read and write. Versioned formats carry their version inside the file.

Entries

An entry is a canonical JSON object with exactly seven members:

MemberContent
specThe spec name, zikaron/1
entryTypeThe type: genesis, history, grant, revocation, adoption, succession, annotation, or any agreed new type
authorThe author’s address, in lowercase hexadecimal
seqThe sequence number, from 0
prevThe ID of the entry before; null for the first
bodyThe body, by type
sigA 65-byte EIP-191 signature

An entry’s ID is the SHA-256 of all its bytes. The canonical form: compact, members sorted by bytes, integers in decimal, hexadecimal always lowercase. See law §3 and §4.

The command line’s ledger folder

A plain folder with one file per entry, each named with the entry ID’s 64 hexadecimal digits plus .entry, readable and writable by its owner alone from the moment it is made. This is the folder zikaron writes; the reading subcommands also take ledgers in desktop data folders (with the credential), ledger mirrors and record kits (a .zkit file or a folder), see Command line. A strict read names a zero-length entry file.

The machine folder

One per machine. On macOS and Linux its default location is ~/.zikaron-desk.d/, and the pointer file ~/.zikaron-desk can point it elsewhere: one line holding an absolute path, ending in a newline, readable and writable by its owner alone. On Windows the pointer file and the default machine folder both sit in %LOCALAPPDATA%\ZIKARON\: .zikaron-desk and .zikaron-desk.d\. On every start the app writes the pointer to the machine folder it uses, keeping it as it is when it already points there. The machine folder is looked for in these two places alone: the pointer first, then the default location; the command line looks in the same order.

FileContent
keys-anchor.jsonThe key store, format zikaron-desk/keybox/3, owner-only; the wrong-try count is kept here too
identities-anchor.jsonThe identity registry, encrypted; the network each identity chose is kept here too
machine.jsonMachine settings, format zikaron-desk/machine/1, plain, read before unlocking, holding the language lang and the appearance appearance alone; any other member is left unread and goes at the next save
machine-anchor.sealedThe encrypted machine settings (kind machine-sealed, format zikaron-desk/machine/1), read once unlocked: auto-lock, the network a new identity selects first network, the proxy proxy (system, none or a proxy address, with its user name and password), putting on chain from the command line cli_anchor (queue for Queue only; absent means send), Through the desktop app cli_desktop (on when absent), With a credential cli_alone (the credential’s lifetime in seconds, or never; off when absent), the data folders used homes, the last whole-machine backup backup and the time of the last failed backup backup_failed; every other member is kept as it is
machine-markThis machine’s mark, 32 lowercase hexadecimal digits and a newline, plain, made the first time it is needed; data folders’ writer marks are compared against it
read-networks.jsonThe read-only networks, format zikaron.read-networks/1, plain, shared by every identity, with no file when there are none; the command line reads it too when it looks for a recipient’s key with the credential
checked/facts.jsonChecked facts, format zikaron.checked-facts/1, encrypted, carried by no mirror, backup or export
kits/index.jsonThe record kit index, plain; see below
kits/links.jsonHand-filled kit fetch addresses, encrypted
kits/verified/Verification result files, plain; see below
records/index.jsonFile names and locations at signing, encrypted; files the command line records with history --file and the credential go here too
backup/index.jsonWhich entries and grants the last whole-machine backup holds, format zikaron-desk/backup-index/1, encrypted; the backup lamp counts what came after from it; it stays on this machine, and a restore writes its own
cli-sent/The transactions the command line signed running anchor with its own key, plain: one folder per anchoring, named by the first 16 bytes (32 hexadecimal digits) of the SHA-256 of the chain ID, the sending account, the recipient (the registry, or the sender itself for a self-transfer) and the call data together; inside, files numbered 0, 1, 2, each {"nonce","tx"} or a void mark {"void"}; the nonce and the transaction hash are all it keeps, and files are only ever added
cli-credential-anchor.jsonThe command line’s credential, there only while “With a credential” is on, owner-only; canonical JSON with exactly four members: expires (Unix seconds, null for never), issued (Unix seconds), key (a random 32-byte key) and master (a nonce and the master key sealed under that key with XChaCha20-Poly1305, additional data zikaron/cli-credential/1); it lasts a minute to a year or never, checked at every use; removed when the switch goes off, at expiry and at a new master key, kept through a passcode change, never in a backup
adopted-salt, adopted-listThe adoption list, plain: the salt file is 64 lowercase hex digits and a newline; the list has one line per adopted ledger, the SHA-256 of the salt followed by its root entry ID, naming no ledger, path or entry; the command line checks it before writing a plain ledger folder (E_ADOPTED)
handed-pensThe handoff list, plain: one line per ledger handed off or taken, keyed by its root ID blinded under the same salt; the command line and the desktop read it before each write
zikaron-door-0x…The local channel the desktop app opens for the data folder it has open, named by the first 8 bytes of the SHA-256 of the machine folder’s and the data folder’s canonical paths (0x and 16 hexadecimal digits); on macOS and Linux a local socket file in the machine folder, readable and writable by its owner alone; on Windows the named pipe \\.\pipe\zikaron-door-0x…, open to this user alone and closed to other machines; locking, closing the data folder or quitting closes it, and it stays shut while “With a credential” is on
set-aside/Sealed files whose master key is gone, closed for good, kept byte for byte and numbered flat 1, 2, 3; when the folder exists, set-aside-2, -3

On macOS and Linux the machine folder is found from HOME; on Windows, from USERPROFILE (the system’s user profile folder when it is unset): when that is the system’s user profile folder, in %LOCALAPPDATA%\ZIKARON\, and otherwise in AppData\Local\ZIKARON\ under it. The app uses its own key store alone and leaves the system keychain as it is.

Data folders

One for each role of each identity, by default inside the machine folder, with the role subfolder author (Recorder) or grantee (User). A data folder stores relative paths alone, so a copy anywhere else, or on another machine, reads back byte for byte. Inside are four subfolders:

ledger/
The ledger, one encrypted file per entry.
kits/
Exported record kits, grant files, credentials and snapshots, and the terms files kept at signing (kits/terms/).
grants-held/
Received grants, each with the verdict of its last re-verification; grant files received are in grants-held/files/.
settings/
Settings desk.json (with this data folder’s network, auto put on chain and “Hide entries deleted on this machine”), the pending queue queue.json, the last ledger check last-audit.json, the encrypted folder label home-label.json (this data folder’s random number, and which role of which identity it belongs to), the plain writer mark writer-mark (one line naming the machine that writes it), and the lock writer.lock that lets one writer in at a time (while “With a credential” is on, the desktop and the command line each take it for a write and let it go after).

Data folder names and the file names in the ledger are rewritten under a local names key, which keeps addresses and entry IDs out of the file names; contents are encrypted file by file. To let another tool read a ledger, export a ledger mirror.

The write lock is held by the operating system’s kernel and is let go the moment the process ends, power loss and forced quits included; it stays with the app’s own process, and programs started from the app run in processes of their own. A kernel lock covers one machine, so each data folder also has a writer mark: when it names another machine, or reads as something this version does not know, the window opens the data folder read-only, and rewrites the mark and takes over once you choose “Write from this machine”. Opening also reads the folder label first: a label belonging to another identity or role is refused, and that data folder is left as it is. A data folder’s label is written by the first window that opens it as the writer or writes into it; a reader writes none.

Encryption of local data

The local data key is derived from the master key with HKDF-SHA256 (info string zikaron/local/v1), and the names key uses the info string zikaron/names/v1. Each file is encrypted on its own with XChaCha20-Poly1305, with a fresh nonce for every seal. The full standard and its test vectors are in the source at crates/app/local-data-standard.md.

"zikaron-local/2\n" · algorithm (1 byte) · kind length (1 byte) · kind · version (u16 big-endian) · identity digest (16 bytes) · nonce (24 bytes) · ciphertext and tag
Algorithm
2 alone: the second half of the identity digest is keyed. An algorithm number below 2, and the magic zikaron-local/1, read as not-sealed; one above 2 reads as newer-version.
Identity digest
A file’s identity is its owner and its place. A file in the machine folder belongs to machine; a file in a data folder belongs to that folder’s random number, kept in its folder label. The place is the kind plus the file’s logical name in its folder (which slot, which name), kept apart from its path and its name on disk, so after moving a data folder, renaming everything under a new master key, or restoring onto another machine, every file opens as before. The first 8 bytes open sha256("zikaron-local/owner" ‖ 0 ‖ owner); the last 8 open HMAC-SHA256(K, "zikaron-local/place" ‖ 0 ‖ logical name), with K derived from the local data key by HKDF-SHA256 under the info string zikaron-local/place-key. Matching a file head to an entry or grant ID therefore takes the key.
Associated data
The whole head, nonce included. A head with any byte changed, or a file moved to another place or another data folder, does not open. The plaintext repeats the owner and the logical name, and both are checked after opening.

One envelope is read and written, the present one: plain files and any other envelope read as not-sealed, named and left as they are, with nothing converted. This version and 0.1.3 write the same envelope; 0.1.2 and earlier cannot read it. How data an earlier version wrote comes across is in Compatibility.

A file that does not read is refused as LOCAL_SEAL, its detail naming one of six reasons: not-sealed (the magic missing, or another envelope or algorithm), truncated (the head or the tag cut short), other-kind (sealed as another kind), newer-version (a later version or algorithm), swapped (another file: the digest differs from the one asked for, or the data folder lacks its label), unopenable (another key, or a byte altered). A file that does not read is left as it is: a write to it is refused by name; move the file aside and the next write makes it anew. Write-once files (held grants, kept grant files, terms files and issuance records, ledger entries) are refused whenever one is already there.

A write seals first, then lands through a temporary file renamed into place, mode 0600; on Windows, a rename that meets a file another program holds for a moment is retried within one second. Entries land on FAT and exFAT volumes as well. A write that fails to land removes its own temporary file, the writer opening a data folder sweeps away any left over, and one left over leaves the ledger readable. An older copy of the same file put back in its place goes unnoticed by the envelope; the ledger has its own signature chain.

The key store

The master key and each identity’s keys are sealed in keys-anchor.json: the passcode derives a key through scrypt (N=262144, r=8, p=1), encryption is AES-128-CTR, and a keccak MAC checks it. The passcode is 8 ASCII letters or digits; after 5 wrong tries in a row it locks, and recovery is the way on.

Whole-machine backups

File name zikaron-backup-YYYY-MM-DD.zikaron (UTC date); a second export on the same day is numbered -2, -3. The first line is zikaron-backup/1, and the second is a canonical JSON header: app, created, format, kdf (scrypt’s N, r, p and a 32-byte salt) and nonce. The body’s key is derived from the backup password with scrypt (N=262144, r=8, p=1), and the body is encrypted with XChaCha20-Poly1305, with the two header lines as associated data.

A backup holds the identity registry, each identity’s keys, every encrypted local file (opened, then placed inside), the record of the primary identity, the machine settings (machine.json and the encrypted machine settings as one) and the read-only networks list (member readNetworks). The command-line credential stays out of every backup, and a restore leaves “With a credential” off. The master key, the passcode and the wrong-try count stay on the machine; checked facts, the backup index, the data folders of deleted identities and plain exports (record kits, the index, verification result files, credentials, grant files, mirrors, key files) stay outside the backup too.

A backup writes to a free name alone: when its name is taken, it takes the next free number. It counts once it is read back and opened; a failure at any step is recorded as backup_failed in the encrypted machine settings until the next backup succeeds. On restore, each member’s path is checked, and a member whose path climbs out of its place is refused; a restore makes a new master key; a backup carrying the read-only networks list replaces this machine’s list, and an older backup, which lacks one, leaves this machine’s list as it is.

Key files

A standard Ethereum keystore (V3): scrypt (N=262144, r=8, p=1) and aes-128-ctr, named UTC--<time>--<40-hex address>, mode 0600, encrypted with the file password set at export (at least 8 characters). After writing, the app reads it back and checks its format and address, and the export counts once they match.

On import both derivations read: scrypt, and pbkdf2 (hmac-sha256, 1 to 224 rounds). scrypt’s parameters must stay in bounds: memory 128·r·N at most 230 bytes, p from 1 to 16, total work 128·r·N·p at most 232 bytes; the key store and whole-machine backups read their parameters within the same bounds. A broken file, or one out of bounds, is refused before any derivation.

Record kits

Its format is set by zikaron.kit/1 §7. An export is one .zkit file by default, and a folder to publish at an address; verifying takes either. Laid out, it is:

kit-3fa9c2e1/ manifest.json the manifest entries/<64 hex>.zk1 entries, one file each files/<path> original files and attachments files/verify.md verification notes for the receiver proofs/<path> on-chain proofs

The manifest lists every entry’s ID, every file’s path, size and SHA-256, and the transaction behind each proof; a file in the kit outside the manifest fails the check. The kit ID is the SHA-256 of the manifest’s bytes. Paths inside a kit use a-z, 0-9, ., _ and - alone; when attachments were renamed, files/zikaron-names.json maps original names to kit names, and the rename rule is <rewritten name>-<first eight of the original name’s SHA-256>.<extension>.

An exported kit is named kit- plus the first eight characters of the content fingerprint of the chosen record with the lowest sequence number, a single file with .zkit after it; when the choice holds other entries alone it is kit, and a taken name is numbered -2, -3. An encrypted record package is named kit.zkit by default, taking nothing from its contents, and numbered on when the name is taken.

On export, a line naming the chain the kit is anchored on is added at the end of the manifest’s note note_md:

anchored-on: eip155:<chain-id> · registry <lowercase contract address> · from <start block>

The separator is “space, · (U+00B7), space”, the same in every interface language, taken from this data folder’s network at the moment of export; with no network set, no line is added, and a note already ending in such a line has it taken off first. The line is a tool-layer convention, and the law still reads the anchors on chain alone: a check uses the line to say which network to add.

Encrypted record packages

A record package encrypted to one recipient is one file, keeping the .zkit extension, holding a plain one-file record package byte for byte:

"zikaron-kit-addressed\n" · version (1 byte, now 1) · one-time public key (33 bytes, compressed) segment 0 · segment 1 · … each 64 KiB of plaintext and a 16-byte tag, the last one shorter
Key
Each package draws a fresh one-time private key e, does ECDH on secp256k1 with the recipient’s public key P and takes the shared point’s x coordinate; HKDF-SHA256, salted with the one-time public key followed by P (both compressed), under the information string zikaron-kit-addressed/1, gives a 32-byte cipher key and a 19-byte nonce prefix. The one-time private key is wiped once agreed, so the sender keeps nothing that opens the package.
Segments
Segment i is XChaCha20-Poly1305 under the nonce “prefix · i (4 bytes, big-endian) · last (1 byte)”, with the whole head as additional data; a changed byte, a dropped or appended segment, or two segments swapped all fail a tag.
Head
The format mark, the version and the one-time public key alone, with nothing of the recipient’s address or key; from outside one sees only that it is an encrypted record package, its size and its file name.

To open one, each key on offer agrees with the one-time key and tries the first segment, and the key whose tag passes is the recipient’s; the package is then opened segment by segment into a folder of its own in the system’s temporary place, and a segment that fails removes the folder before the refusal, so no plaintext stays. On export, the plain package is laid out in a folder of its own in the system’s temporary place, encrypted into its destination, and the plain copy removed.

The record kit index

kits/index.json in the machine folder, format name zikaron.kits-index/1, kept plain so that other apps on the same machine can read it; see Integration. Each exported kit gets one row, and exporting again to the same path replaces that row:

FieldContent
idThe kit ID
createdThe moment of export, in Unix seconds
rootThe ID of the ledger’s creation entry
pathThe kit’s absolute path
contentsThe content fingerprints of the originals in the kit
note_mdThe note, as the author wrote it, without the anchored-on line
anchoredOnThe chain the kit is anchored on: {chainId, fromBlock, registry}, optional
linkThe fetch address (the kit’s https address online), optional
oneTimeKeyAn encrypted record package’s one-time public key (hex), by which its local copy is recognized; absent for a plain package. No row records a recipient, and the other cells come from the plain package before it was encrypted

Verification result files

Each kit, grant file or publish address verified writes a result in the machine folder under kits/verified/, named 0x<sha256 of the kit’s manifest file>.json; verifying again replaces it whole. Its format name is zikaron.kit-verification/1, plain and owner-only, in canonical JSON:

{"anchors":[…],"at":<seconds>,"basis":{…}?,"core":<hex32>,"form":"zikaron.kit-verification/1", "kit":<hex32>?,"kitVerdict":{"subject":"…"?,"verdict":"…"},"manifestSha256":<hex32>, "missed":[{"chainId":…,"reading":"down"|"fingerprint","registry":"0x…"}], "records":[{"anchor":{"blockNumber","blockTimestamp","chainId","registry","tx"}|null,"content":<hex32>}]}
records
One row per record in the kit: its content fingerprint and the earliest anchor counted for it; null for a record not on chain, or, when a chain could not be read, for one with no anchor on the chains that were read.
anchors
The anchors this pass found, each with its registry contract.
basis, missed
The chains read this time, and the networks not read with their reading code: down unreachable, fingerprint fingerprint mismatch.
core
The digest of the core that judged.

A kit that does not hold is written too, with its verdict alone; a kit that holds is left unwritten when no chain was read this time. The app writes the file and never reads it back, and the file changes no verdict; see Integrating.

Grant codes and credentials

The text zikaron-grant: followed by one or more segments joined by .. Each segment is a grant entry’s canonical bytes in base64url, padding left off; with several segments, they run from the original author’s grant to the current one. The whole is capped at 2,953 bytes, exactly the capacity of one QR code (version 40, byte mode, error correction level L). The format is in zikaron.kit/1 §6. A pasted grant code first loses its line breaks, spaces and invisible characters (zero-width characters, soft hyphens, byte-order marks), and is then judged by its shape: one of the wrong shape is refused by name.

A credential folder is named badge- plus the first ten characters of the grant ID; inside, badge.txt is the text as it is, and badge.svg is its QR code.

Grant files

Extension .zkgrant, named grant- plus the first ten characters of the grant ID. It is a container: the first line is zikaron-kit-file/1, and each item after it is written as “path in the kit, newline, decimal length, newline, bytes, newline”, the manifest first and the rest in byte order. A length reads in the encoder’s spelling alone: decimal digits, 0 for zero, and otherwise a first digit from 1 to 9. Readers that gather a file into memory (grant files, a kit fetched from a publish address) cap it at 64 MiB in all and 4096 items; a record kit file is written and read item by item, with no cap on its total size. It is read in one place (zikaron_glue::grantfile), shared by the app and the command line’s check-grant. Inside are:

  • manifest.json;
  • entries/*.zk1: the entries of every hop in the grant chain, together with the issuer’s ledger when the grant is in this role’s ledger;
  • files/zikaron-grant.txt (the grant code), files/terms/… (the terms files), files/verify.md, and optionally files/publish.txt (the published address set in the data folder that exported the grant file).

Handoff file

Extension .zkhand, written by hand-over: a record kit in one file, in the grant file’s container, holding every entry of the ledger and files/zikaron-handoff.txt, three lines: handoff/1, id and the handoff’s 32 hexadecimal digits, after and the ID of the handoff its giver took the ledger by (- when it always held it). take-over checks it under the kit law before reading anything in it.

Ledger mirrors

<chosen folder>/ZIKARON-backup/<40-hex address>/<author|grantee>/ mirror.json kind desk-mirror, version 1 entries/<64 hex> entry bytes, named by ID alone held/<path> received grants and so on

When the folder already holds an older mirror, only the new entries are added. The command line’s reading subcommands can point straight at the level holding mirror.json. A mirror’s author folder can be taken into a data folder on the desktop with “Adopt existing ledger folder” and “Adopt in place”.

Derivation paths

Recorderm/44'/60'/0'/0/0
Userm/44'/60'/0'/0/1

Agreed literals

bytes-sha256/1
The mark of how a record was made, one for files, folders and Git repositories alike, meaning the content fingerprint is the SHA-256 of a run of bytes. For a file, the bytes are the whole file; for a folder, the canonical JSON {"files":[{"path":…,"sha256":…}…]} sorted by relative path (symbolic links and special files are skipped and counted, and an empty folder is refused); the names in a path are joined by / on every system, so the same tree gives the same fingerprint anywhere; checking also accepts records that earlier versions signed on Windows with \ joining the names, and signing writes the / spelling alone; for a Git repository, the current commit object’s bytes without the commit <length>\0 header, the same as git cat-file commit HEAD | shasum -a 256. The toolchain paired with it is the SHA-256 of this literal itself: 0xb7f3c4a226684d6da684b0ac82d36ed5bcba7bb61d09c955bff9437f84a52a4d.
retraction
The type name of a deletion entry. In the body, subject is required: the ID of a record in this ledger, 0x followed by 64 lowercase hexadecimal digits; note_md is optional. Other tools list it as an unknown type (UNKNOWN_TYPE) and the ledger’s verdict stays as it was; this app’s reading lives in zikaron_glue::retraction, shared by the app and the command line.
for
An extra member in a record’s body: app, identity (hex20) and ref, with an optional seat. Once any of them is filled in, the first three are required.
upstream
An extra member in a grant’s body, holding the ID of the upstream grant; zikaron.kit/1 reads it as a sublicense.