开发者指南 · Zikaron

文件与格式

应用与命令行读写的各类文件。带版本号的格式,版本号写在文件里。

条目

条目是一个规范 JSON 对象,恰有七个成员:

成员内容
spec规格名,zikaron/1
entryType类型:genesis、history、grant、revocation、adoption、succession、annotation,或任何约定的新类型
author作者地址,小写十六进制
seq序号,从 0 起
prev前一条的编号;第一条为 null
body正文,按类型而定
sig65 字节的 EIP-191 签名

条目编号是它全部字节的 SHA-256。规范格式:紧凑书写,成员按字节序排列,整数按十进制书写,十六进制一律小写。详见法 §3 与 §4。

命令行的账本目录

一个明文文件夹,每条条目一个文件,文件名为条目编号的 64 位十六进制加 .entry,从建起就只有本人能读写。zikaron 写的就是这种目录;读的子命令另外也收桌面数据目录里的账本(经凭据)、账本镜像与记录包(.zkit 文件或文件夹),见命令行。严格读取时,长度为零的条目文件具名报出。

机器目录

每台机器一个。在 macOS 与 Linux 上,默认位置是 ~/.zikaron-desk.d/,指针文件 ~/.zikaron-desk 可以把它指到别处:一行绝对路径,以一个换行结尾,只本人可读写。在 Windows 上,指针文件与默认机器目录都在 %LOCALAPPDATA%\ZIKARON\ 里:.zikaron-desk 与 .zikaron-desk.d\。应用每次启动时把指针写成所用的机器目录,已指向它时原样留着。机器目录只在这两处找:先看指针,再看默认位置;命令行按同样的次序找。

文件内容
keys-anchor.json密钥库,格式 zikaron-desk/keybox/3,只本人可读写;错误次数也记在这里
identities-anchor.json身份登记,加密;每个身份选的网络也记在这里
machine.json机器设置,格式 zikaron-desk/machine/1,明文,开锁前要读,只有语言 lang 与外观 appearance 两项;别的成员不读,下次保存即去掉
machine-anchor.sealed加密的机器设置(种类 machine-sealed,格式 zikaron-desk/machine/1),开锁后才读:自动锁定、新建身份时默认选中的网络 network、代理 proxy(system、none 或一处代理地址,连同用户名与密码)、命令行上链 cli_anchor(queue 为仅加入队列,缺省即自动发送)、经桌面 App 操作 cli_desktop(缺省开)、经凭据操作 cli_alone(凭据的有效秒数或 never,缺省关)、用过的数据目录 homes、上次整机备份 backup 与上次备份失败的时刻 backup_failed;其余成员原样保留
machine-mark本机的机器标,32 位小写十六进制加一个换行,明文,首次用到时生成;数据目录的写者标拿它比对
read-networks.json只读网络,格式 zikaron.read-networks/1,明文,所有身份共用,一个也没有时不留文件;命令行经凭据找接收方公钥时也读它
checked/facts.json已核对事实,格式 zikaron.checked-facts/1,加密,不进任何镜像、备份或导出件
kits/index.json记录包索引,明文,见下文
kits/links.json手填的记录包取处,加密
kits/verified/核验结果档,明文,见下文
records/index.json签入时的文件名与位置,加密;命令行经凭据 history --file 写进来的文件也记在这里
backup/index.json上次整机备份收了哪些条目与授权,格式 zikaron-desk/backup-index/1,加密;备份灯据它数出备份之后新增的条数;不进备份,恢复时另写一份
cli-sent/命令行自持私钥运行 anchor 时签过的交易,明文:每次锚定一个文件夹,名为链号、发送账户、去处(登记合约,自转账时即发送账户本身)与调用数据合起来的 SHA-256 前 16 字节(32 位十六进制);里面按 0、1、2 编号,每个文件是 {"nonce","tx"},或标记作废的 {"void"};只记 nonce 与交易哈希,文件只增
cli-credential-anchor.json命令行的凭据,「经凭据操作」开着时才有,只本人可读写;规范 JSON,恰有四个成员:expires(到期的 Unix 秒,不过期为 null)、issued(签发的 Unix 秒)、key(一把随机的 32 字节钥)、master(随机数加以这把钥经 XChaCha20-Poly1305 封起来的主钥,附加数据 zikaron/cli-credential/1);有效期一分钟到一年或不过期,每次用都核期限;关掉开关、到期、换主钥即删,改口令照用,不进备份
adopted-salt、adopted-list收编清单,明文:盐文件是 64 位小写十六进制加一个换行;清单每本收编的账一行,内容是盐加根条目号的 SHA-256,不含账本名、路径或条目号;命令行写明文账本目录之前据它判 E_ADOPTED
handed-pens交接清单,明文:每本交出或收下的账一行,以同一把盐盲化的根条目号为键;命令行与桌面写之前都先读它
zikaron-door-0x…桌面应用为打开的数据目录开的本机通道,名字取机器目录与数据目录两条规范路径的 SHA-256 前 8 字节(0x 加 16 位十六进制);macOS 与 Linux 上是机器目录里的本机套接字文件,只本人可读写;Windows 上是命名管道 \\.\pipe\zikaron-door-0x…,只本用户可连、拒收别的机器;锁定、关闭数据目录或退出时关上,「经凭据操作」开着时不开
set-aside/主钥已失、永久封存的文件,字节原样,按 1、2、3 编号平铺;目录已有时为 set-aside-2、-3

在 macOS 与 Linux 上,机器目录从 HOME 算起;在 Windows 上从 USERPROFILE 算起(未设时取系统的用户配置文件夹):它就是系统的用户配置文件夹时在 %LOCALAPPDATA%\ZIKARON\,否则在它下面的 AppData\Local\ZIKARON\。应用只用自己的密钥库,系统钥匙串保持原样。

数据目录

每个身份的每种角色一个,默认放在机器目录下,角色子目录为 author(记录者)或 grantee(使用方)。数据目录里不存任何绝对路径,整个复制到别处或别台机器,原样读回。里面四个子目录:

ledger/
账本,每条条目一个加密文件。
kits/
导出的记录包、授权文件、凭证、快照,以及签发时留存的条款文件(kits/terms/)。
grants-held/
收到的授权,每份附上次复核的结论;收下的授权文件在 grants-held/files/。
settings/
设置 desk.json(含这个数据目录的网络、自动上链与「隐藏本机删除的条目」)、待上链队列 queue.json、上次账本检查 last-audit.json、加密的目录标签 home-label.json(这个数据目录的随机编号,以及它属于哪个身份的哪种角色)、明文的写者标 writer-mark(一行,写着由哪台机器写入),以及只让一处写入的锁 writer.lock(「经凭据操作」开着时,桌面与命令行每写一笔各拿一次,写完即放)。

数据目录的文件夹名和账本里的文件名,都由一把本机名钥转写,地址与条目编号由此从文件名上隐去;内容逐文件加密。要给别的工具读,导出账本镜像。

写入锁由系统内核持有,进程一结束即放开,断电与强行结束也一样;它只归应用自己的进程,从应用里启动的程序另起自己的进程。内核锁只管一台机器,所以另有写者标:打开一个数据目录时,写者标写着别台机器,或这一版读不成,这个窗口就只读,等你选「改由本机写」才改写写者标并接管。打开时还先读目录标签,标签属于别的身份或角色即拒,这个数据目录原样留着。数据目录的标签由第一个以写者打开或写进它的窗口写下,只读打开不写。

本机数据的加密

本机数据钥由主钥经 HKDF-SHA256 派生(信息串 zikaron/local/v1),文件名钥另用信息串 zikaron/names/v1。每个文件以 XChaCha20-Poly1305 单独加密,每次封装都用新的随机数。完整的规格与测试向量在源码的 crates/app/local-data-standard.md。

"zikaron-local/2\n" · 算法(1 字节) · 种类长度(1 字节) · 种类 · 版本(u16 大端) · 身份摘要(16 字节) · 随机数(24 字节) · 密文与标签
算法
只认 2:身份摘要的后半由钥算出。低于 2 的算法号与魔数 zikaron-local/1 读作 not-sealed,高于 2 的读作 newer-version。
身份摘要
一个文件的身份是它的主人与位置。机器目录里的文件主人是 machine;数据目录里的文件主人是这个目录的随机编号,记在它的目录标签里。位置是种类加上文件在目录里的逻辑名(哪一格、哪个名字),与磁盘上的路径和文件名分开,所以挪动数据目录、换主钥后整批改名、恢复到别台机器之后,每个文件照旧打开。前 8 字节是 sha256("zikaron-local/owner" ‖ 0 ‖ 主人) 的开头;后 8 字节是 HMAC-SHA256(K, "zikaron-local/place" ‖ 0 ‖ 逻辑名) 的开头,K 由本机数据钥经 HKDF-SHA256 以信息串 zikaron-local/place-key 派生。这样,文件头与条目编号、授权编号之间的对应,只有持钥的人算得出。
附加数据
整个文件头,连同随机数。头里任一字节改动,或文件被挪到别的位置、别的数据目录,都打不开。明文里再写一遍主人与逻辑名,打开后逐项核对。

读与写都只用现行这一种封装:明文文件与别的封装读作 not-sealed,点名原样留着,不转换。本版与 0.1.3 写的是同一种封装;0.1.2 及更早版本读不了。早先版本的数据怎样带过来,见兼容与演进。

读不成的文件统一报 LOCAL_SEAL,详情写明六种原因之一:not-sealed(缺魔数,或别的封装、算法号)、truncated(头或标签被截断)、other-kind(封的是别的种类)、newer-version(更新的版本或算法写的)、swapped(别的文件:摘要与所求的不符,或数据目录缺标签)、unopenable(别的钥,或有字节被改)。读不成的文件原样留着:写到它时具名拒绝,把它挪开,下次写入即重建。只写一次的文件(收到的授权、留存的授权文件、条款文件与签发记录、账本条目)只要已在,一律拒写。

写入时先封装,再经临时文件改名到位,权限 0600;改名在 Windows 上遇到别的程序一时占着文件,在一秒内重试。在 FAT 与 exFAT 卷上也能落条目。写不成的写入随即删掉自己的临时文件,写者打开数据目录时扫掉残留的,残留的临时文件不挡读账本。同一文件的较早副本放回原处,封装认不出来;账本有自己的签名链。

密钥库

主钥与各身份的密钥封在 keys-anchor.json 里:口令经 scrypt(N=262144、r=8、p=1)派生密钥,以 AES-128-CTR 加密,keccak MAC 校验。口令为 8 位 ASCII 字母或数字,连错 5 次锁定,只能走恢复。

整机备份

文件名 zikaron-backup-YYYY-MM-DD.zikaron(UTC 日期),同一天再导出编号为 -2、-3。第一行为 zikaron-backup/1,第二行是规范 JSON 文件头:app、created、format、kdf(scrypt 的 N、r、p 与 32 字节盐)、nonce。正文用 scrypt(N=262144、r=8、p=1)从备份密码派生密钥,以 XChaCha20-Poly1305 加密,两行文件头作为附加数据。

备份里有:身份登记、每个身份的密钥、每一份加密的本机文件(解开后放入)、主身份的记录、机器设置(machine.json 与加密的机器设置合成一份)与只读网络列表(成员 readNetworks)。命令行的凭据不进备份,恢复之后「经凭据操作」是关的。主钥、口令与错误次数留在本机;已核对事实、备份索引、已删除身份的数据目录,以及明文的导出件(记录包、索引、核验结果档、凭证、授权文件、镜像、密钥文件)也在备份之外。

备份只写进空着的名字:要用的名字已被占用时,顺延取下一个编号。写出后读回并打开,才算备份成功;任何一步失败,加密的机器设置都记下 backup_failed,直到下一次备份成功。恢复时逐个检查成员的路径,带路径穿越的成员一概拒收;恢复生成新的主钥;备份带着只读网络列表时,替换本机的列表;旧版的备份缺这一项,本机的列表保持原样。

密钥文件

标准以太坊 keystore(V3):scrypt(N=262144、r=8、p=1)、aes-128-ctr,文件名为 UTC--<时间>--<40 位十六进制地址>,权限 0600,以导出时设定的文件密码(至少 8 位)加密。写出后应用读回核对格式与地址,相符才算导出成功。

导入时两种派生都认:scrypt,以及 pbkdf2(hmac-sha256,轮数 1 至 224)。scrypt 参数须在界内:内存 128·r·N 至多 230 字节,p 为 1 至 16,总工作量 128·r·N·p 至多 232 字节;密钥库与整机备份读参数时用同一组界限。坏的或超界的文件在派生之前即拒。

记录包

格式由 zikaron.kit/1 §7 规定。导出缺省是一个 .zkit 文件,要发布到网址时导出为文件夹;核验时两种都收。展开后是:

kit-3fa9c2e1/ manifest.json 清单 entries/<64 位十六进制>.zk1 条目,每条一个文件 files/<路径> 原文件与附件 files/verify.md 给收件方的核验说明 proofs/<路径> 上链证明

清单列出每个条目的编号,每个文件的路径、大小与 SHA-256,以及每份证明对应的交易;包里出现清单之外的文件,核验即失败。记录包编号是清单字节的 SHA-256。包内路径只用 a-z、0-9、.、_、-;有附件改过名时,files/zikaron-names.json 记下原名与包内名的对照,改名规则为 <转写名>-<原名 SHA-256 前八位>.<扩展名>。

导出的记录包名为 kit- 加所选里序号最小的那件记录的内容指纹前八位,单个文件再加 .zkit;所选只含其他条目时为 kit,同名已有即编号为 -2、-3。加密记录包缺省名为 kit.zkit,不取包里的任何内容,同名已有即顺延编号。

导出时,清单里附言 note_md 的末尾自动加一行,写明锚在哪条链:

anchored-on: eip155:<链号> · registry <小写合约地址> · from <起始区块>

分隔符是「空格、·(U+00B7)、空格」,不随界面语言变,取自导出那一刻这个数据目录的网络;没配网络就不加,附言原本以这样一行结尾时先去掉再加。这一行属工具层的约定,法照旧只读链上的锚:核验时据它提示要添加的网络。

加密记录包

加密给一个接收方的记录包是单个文件,扩展名照用 .zkit,里面是逐字节原样的普通单文件记录包:

"zikaron-kit-addressed\n" · 版本(1 字节,现为 1) · 一次性公钥(33 字节,压缩) 段 0 · 段 1 · … 每段 64 KiB 明文加 16 字节标签,末段较短
密钥
每个包新取一把一次性私钥 e,与接收方公钥 P 在 secp256k1 上做 ECDH,取共享点的 x 坐标;HKDF-SHA256 以一次性公钥与 P(都压缩)相接为盐、信息串 zikaron-kit-addressed/1,导出 32 字节加密钥与 19 字节随机数前缀。一次性私钥用完即抹,发送方自己留不下可解的钥。
分段
第 i 段以 XChaCha20-Poly1305 加密,随机数为「前缀 · i(4 字节大端) · 是否末段(1 字节)」,整个文件头作附加数据;改一个字节、删段、补段、换段次序,都过不了标签。
文件头
只有格式标记、版本与一次性公钥,不写接收方的地址或公钥;外人只看得出它是加密记录包、大小与文件名。

打开时,每把候选钥匙与一次性公钥协商后试第一段,标签过得去的那把就是接收方;之后逐段解进系统临时目录里专用的文件夹,有一段不过即删掉整个文件夹再报拒,所以不留明文。导出时,明文包先排在系统临时目录里专用的文件夹,加密写到目标位置,明文副本随即删掉。

记录包索引

机器目录下的 kits/index.json,格式名 zikaron.kits-index/1,明文保存,供同一台机器上的其他应用只读访问,见接入。每导出一个记录包记一行,同一路径再导出时替换原行:

字段内容
id记录包编号
created导出时刻,Unix 秒
root账本创建条目的编号
path包的绝对路径
contents包内原件的内容指纹
note_md附言,作者写的原文,不含 anchored-on 那一行
anchoredOn锚在哪条链:{chainId, fromBlock, registry},可选
link取处(这个包在网上的 https 地址),可选
oneTimeKey加密记录包的一次性公钥(十六进制),用来认出本机那一份;普通包没有这一项。行里不记接收方,其余各项读自加密前的明文包

核验结果档

每核验一个记录包、授权文件或发布地址,应用在机器目录的 kits/verified/ 里写一份结果,文件名是 0x<包清单文件的 sha256>.json,再核整份替换。格式名 zikaron.kit-verification/1,明文、只本人可读写,规范 JSON:

{"anchors":[…],"at":<秒>,"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
包里每件记录一行:内容指纹,以及最早计入的那一枚锚;没上链的,或有链没读成而读成的链上找不到锚的,写 null。
anchors
这一次扫到的锚,各带它的登记合约。
basis、missed
这一次读成的链,与没读成的网络及其读法代码:down 不通,fingerprint 指纹不符。
core
判这一次的核的摘要。

包不成立也写,只写判定;包成立而这一次一条链也没读成时不写。应用自己只写不读,这份档也不改任何判定,见接入。

授权码与凭证

文本 zikaron-grant: 后接一段或多段,段间以 . 连接。每段是一条授权条目的规范字节,经 base64url 编码(省去填充);多段时,从最初作者的授权依次排到当前这一份。全长上限 2,953 字节,恰为一个二维码的容量(版本 40、字节模式、纠错等级 L)。格式见 zikaron.kit/1 §6。粘贴进来的授权码,先去掉换行、空格与看不见的字符(零宽字符、软连字符、字节序标记),再判形:形不对的具名拒收。

凭证文件夹名为 badge- 加授权编号前十位,里面的 badge.txt 是原样的文本,badge.svg 是它的二维码。

授权文件

扩展名 .zkgrant,文件名为 grant- 加授权编号前十位。它是一个容器:第一行 zikaron-kit-file/1,之后每一项写成「包内路径、换行、十进制长度、换行、字节、换行」,清单在前,其余按字节序。长度只认编码器的写法:十进制数字,零写作 0,其余以 1 至 9 开头。读进内存的读法(授权文件、从发布地址取来的记录包)上限为总长 64 MiB、4096 项;记录包文件逐项写、逐项读,总长不设上限。它的读法只有一处(zikaron_glue::grantfile),应用与命令行的 check-grant 共用。里面装着:

  • manifest.json;
  • entries/*.zk1:授权链各跳的条目,授权在本角色账本里时,连同签发者账本;
  • files/zikaron-grant.txt(授权码)、files/terms/…(条款文件)、files/verify.md,以及可选的 files/publish.txt(导出这份授权文件的数据目录所设的发布地址)。

交接件

扩展名 .zkhand,由 hand-over 写出:与授权文件同一个容器的单文件记录包,装着整本账的条目与 files/zikaron-handoff.txt。后者三行:handoff/1、id 加 32 位十六进制的交接编号、after 加交出方当初收下这本账所凭的那份交接件的编号(一直自己持有时写 -)。take-over 先按记录包的法核一遍,再读其中任何一项。

账本镜像

<所选文件夹>/ZIKARON-backup/<40 位十六进制地址>/<author|grantee>/ mirror.json 种类 desk-mirror,版本 1 entries/<64 位十六进制> 条目原文,文件名只有编号 held/<路径> 收到的授权等

同一文件夹里已有较早的镜像时,只补上新增的条目。命令行读的子命令可以直接指向含 mirror.json 的那一层。镜像里的 author 文件夹可以在桌面上用「收编已有账本目录」「原位收编」收进数据目录。

派生路径

记录者m/44'/60'/0'/0/0
使用方m/44'/60'/0'/0/1

约定的字面量

bytes-sha256/1
存证的方式标记,文件、文件夹与 Git 仓库共用这一个,表示内容指纹是一串字节的 SHA-256。对文件,这串字节是文件的全部字节;对文件夹,是按相对路径排序的规范 JSON {"files":[{"path":…,"sha256":…}…]}(符号链接与特殊文件略过并计数,空文件夹拒收);路径里的各级名字在任何系统上都以 / 连接,同一棵树在哪里算都是同一个指纹;核对时也认早先版本在 Windows 上以 \ 连接写下的记录,签名只写 / 这一种;对 Git 仓库,是当前提交对象去掉 commit <长度>\0 头之后的字节,与 git cat-file commit HEAD | shasum -a 256 相同。与之配对的 toolchain 是这个字面量本身的 SHA-256:0xb7f3c4a226684d6da684b0ac82d36ed5bcba7bb61d09c955bff9437f84a52a4d。
retraction
删除条目的类型名。正文中 subject 必填,为本账本里一条存证的编号,0x 加 64 位小写十六进制;note_md 可选。别的工具把它列为未知类型(UNKNOWN_TYPE),账本结论照旧;本应用的读法写在 zikaron_glue::retraction,应用与命令行共用。
for
存证正文里的附加成员:app、identity(hex20)、ref,另有可选的 seat。填了任一项,前三项都要有。
upstream
授权正文里的附加成员,值为上游授权的编号,zikaron.kit/1 把它读作转授权。