Files for this edition
- SPEC-v1.2.md: the specification in Markdown, as authored
- SPEC-v1.2-vectors.json: known-answer vectors (test keys only)
- verify-v1.2.mjs: an independent verifier that opens every vector (
node verify-v1.2.mjs) - SHA256SUMS: SHA-256 of each file
Rootz Data-Confidentiality Protocols — Specification v1.2
| Version | 1.2 (supersedes 1.1) |
| Date | 2026-10-16 |
| Publisher | Rootz Corp. |
| Authorship | Prepared with AI assistance (Claude). |
| Companion files | SPEC-v1.2-vectors.json (known-answer vectors, test keys only); verify-v1.2.mjs (an independent consumer that opens every vector) |
1. Scope
1.1 What is specified. This document specifies the data-confidentiality constructions in Rootz Corp products (the Rootz Layer, Rootz Desktop, the Rootz web applications, Rootz SDK packages, Rootz Sealed Memory, the Rootz proof binder), in the forms in which Rootz distributes or delivers them to third parties:
- (a) public builds and installers, including Rootz Desktop and the web application code served to browsers (the Rootz dashboard and the dive application);
- (b) SDK and library packages published or supplied by Rootz; and
- (c) source code supplied to third parties.
Components that are not yet released are included and marked as such (Section 4.9).
A data-confidentiality construction is any procedure in that code that encrypts data, wraps (encrypts) a key, or derives a key that is then used for encryption or unwrapping. Appendix A lists each construction with the products and packages in which it occurs.
The document is written so that a competent cryptographer, using only this document and the cited standards, can build an implementation that decrypts data produced by Rootz software when given the key, and can produce data that Rootz software will decrypt.
1.2 Out of scope by function. Functions whose only purpose is authentication, digital signature or data integrity are not specified: ECDSA, EIP-191, EIP-712, EIP-1271, Ed25519, ML-DSA, RSA signatures, Merkle trees and document hashes. A signature or hash appears here only where its output is an input to key derivation, or where it is a field of an encrypted envelope that a consumer must parse or check; there it is specified only as far as interoperability requires.
1.3 Also out of scope:
- in this edition, the constructions of the Epistery library other than the peer-cipher wrap (A.3). Epistery is a Rootz Corp project. Its remaining constructions, including its TreeKEM group key schedule, will be specified in version 1.3 of this document;
- standard formats that Rootz software reads or writes through third-party libraries, which are cited by reference and not re-specified (for example the Ethereum keystore format noted in C.3);
- key-management policy, key distribution between applications, deployment, and storage locations;
- non-cryptographic application logic; and
- code that runs only on Rootz's own servers and is not distributed.
1.4 What this document is not. It is a specification, not source code. It contains no
production keys. All private keys, seeds and content keys shown are test material (repeated-byte
patterns such as 0x1111…11). G.1 uses a generated RSA test key, of which only the public half is
published. The one value that is not test material is the G.2 anchor A: it is a fixed protocol
public key, embedded in the application, and is published here because consumers need it.
1.5 How statements were established. Unless marked otherwise, each byte-level statement was established by running the shipping implementation with test keys and opening its output with an independent implementation (Appendix B). A statement marked [code-read] was established by reading the source code and was not executed.
2. Composition principle
Every construction in this document is a composition of named, published, standard primitives (Section 3). Rootz defines no new cipher, hash function, key-encapsulation mechanism, key-agreement primitive, key-derivation function or mode of operation. Each entry specifies the composition: which primitive outputs feed which inputs, the exact byte encodings, the fixed labels and the wire format. Each entry carries a generic name, which describes the composition in standard terms, and a Composition of line, which cites the standards it composes.
Three recurring patterns are described once here:
- Hash of a shared secret as a key (
K = SHA-256(Z),K = Keccak-256(Z)). This is a single application of the FIPS 180-4 or FIPS 202 function to the key-agreement output. It is a composition of a standard hash with a standard key agreement, not a new KDF algorithm. It differs from the SP 800-56C one-step KDF and the ANSI X9.63 KDF in that no counter or other information is appended. - Hash of a deterministic signature as a key (B.1, B.2, G.1). A deterministic signature (RFC 6979 ECDSA, or RSASSA-PKCS1-v1_5) over a fixed message is used as secret input to a single standard hash. This too is a composition of standard functions, not a new KDF algorithm.
- ECIES-style and static-static wraps. The ephemeral forms (A.1 device and package forms, A.4 as normally used, the A.5 package, D.2 classical, E.1, and the A.5 single wrap and A.6 when the caller supplies an ephemeral key) follow the structure of SEC 1 v2.0 §5.1 ECIES. The static-static forms (A.1 member wrap, A.2, A.3, the classical half of D.5, and A.4, the A.5 single wrap and A.6 when the sender's long-term key is used) follow the structure of the SP 800-56A Rev. 3 §6.3 C(0e, 2s) scheme. Each entry states its deviations: the KDF (above), the shared-secret encoding (Section 4.3), and the use of AES-GCM authenticated encryption in place of a separate encryption and MAC step.
3. Primitives and standards
| Primitive | Standard | Used as |
|---|---|---|
| AES-256 | FIPS 197 (2001, upd. 2023) | Block cipher under GCM |
| AES-GCM, 128-bit tag | NIST SP 800-38D | All symmetric encryption in this document |
| secp256k1 elliptic curve | SEC 2 v2.0 §2.4.1 | ECDH key agreement; private keys |
| EC point encoding, EC Diffie-Hellman | SEC 1 v2.0 §2.3.3–2.3.4 (encoding), §3.3.1 (ECDH) | Shared-point computation; 65-byte / 33-byte public keys |
| ECIES structure | SEC 1 v2.0 §5.1 | Structure of the ephemeral wraps (deviations stated per entry) |
| NIST P-256 | FIPS 186-5 with NIST SP 800-186 (curve) | Sealed box (E.1); Secure Enclave key (G.2) |
| ECC CDH shared secret Z; C(0e, 2s) scheme | NIST SP 800-56A Rev. 3 §5.7.1.2, §6.3 | x-coordinate shared secret; structure of static-static wraps |
| Hybrid shared secret; two-step key derivation | NIST SP 800-56C Rev. 2 §2 (Z′ = Z ‖ T), §5 (extraction-then-expansion) |
D.5 combiner and HKDF |
| KEM usage | NIST SP 800-227 | Use of an ML-KEM shared secret as KDF input (D.1, D.4, D.5) |
| X25519 | RFC 7748 §5 | Hybrid KEM classical half (D.3) |
| ML-KEM-1024 | FIPS 203 | Post-quantum key encapsulation |
| SHA-256, SHA-384 | FIPS 180-4 | KDFs, salts, chain hashes |
| SHA3-256 | FIPS 202 §6.1 | Hybrid KEM combiner (D.3) |
| Keccak-256 | FIPS 202 §5.2: KECCAK[512](M, 256), i.e. the sponge of §4 over Keccak-p[1600, 24] (§3) with pad10*1 (§5.1), rate 1088 bits, without the SHA-3 domain suffix 01 |
KDFs. The first padding byte is 0x01; SHA3-256 (suffix 01, first padding byte 0x06) gives different output. This is the function Ethereum calls keccak256 |
| HKDF | RFC 5869 | KDF (SHA-256 and SHA-384 variants) |
| HMAC | FIPS 198-1 (= RFC 2104) | HKDF; record naming |
Referenced only where they feed key derivation or a parsed field (Section 1.2):
| Primitive | Standard |
|---|---|
| Deterministic ECDSA | RFC 6979 (secp256k1, low-s normalisation) |
| Ethereum signed-message prefix | EIP-191 version 0x45 ("personal_sign") |
| Ethereum address and checksum | Ethereum Yellow Paper (address = last 20 bytes of Keccak-256 of the 64-byte public key X‖Y); EIP-55 (mixed-case checksum) |
| RSASSA-PKCS1-v1_5 | RFC 8017 §8.2, with SHA-256 |
| ML-DSA-65 | FIPS 204 (pure mode, empty context string) |
| Shamir secret sharing | A. Shamir, "How to share a secret", CACM 22(11), 1979; field GF(2^8) with reduction polynomial 0x11b as in FIPS 197 §4.2 |
| Base64 | RFC 4648 §4 (standard alphabet, with padding) |
| JSON | RFC 8259; where bytes of a JSON text are hashed, MACed or used as AAD, the exact serialisation is specified in the entry |
| ISO 8601 date-time | Seat timestamps (D.5) |
| Ethereum keystore | Web3 Secret Storage Definition, version 3 (cited by reference only; see C.3) |
4. Conventions
4.1 Notation. ‖ is byte concatenation. utf8(s) is the UTF-8 encoding of string s.
hex(b) is lower-case hexadecimal with no prefix; hex0x(b) is "0x" ‖ hex(b). "0x-hex" in a wire
format means hex0x. len(b) is a length in bytes. rand(n) is n bytes from a cryptographically
secure random number generator. pub(d) is the public key of private key d.
4.2 AES-GCM. GCM-Enc(K, IV, P, A) returns (C, T) per SP 800-38D with a 32-byte key, a
16-byte tag T, and len(C) = len(P). A is empty unless the entry says otherwise. The IV is
12 bytes (96 bits) unless the entry says otherwise. Two tag placements occur, and each entry states
which:
- tag appended: one field carries
C ‖ T; - tag split:
CandTare carried in separate fields.
4.3 secp256k1 ECDH. For a private key d and a public key Q, let P = d·Q be the shared
point (SEC 1 §3.3.1, step 1). Two different byte strings are derived from P, and they are not
interchangeable:
Z65(P) = 0x04 ‖ X ‖ Y— the 65-byte SEC 1 uncompressed encoding of the whole point;X32(P) = X— the 32-byte big-endian x-coordinate (the SEC 1 / SP 800-56A shared secret).
Producers emit public keys as the 65-byte uncompressed encoding 0x04 ‖ X ‖ Y unless an entry
states the 33-byte compressed form (0x02/0x03 ‖ X, SEC 1 §2.3.3). Where an entry permits
compressed keys (A.5), a consumer decompresses them per SEC 1 §2.3.4; the shared point, and therefore
Z65, is the same. Elsewhere, shipping consumers accept only the 65-byte form (for example A.1), so a
producer must not emit a compressed key where the entry does not permit one.
4.4 The secp256k1 key-derivation families. A wrap made in one family does not open in another (verified by execution for the first three):
| Family | Key derivation | Constructions |
|---|---|---|
| SHA-256 over the 65-byte point | K = SHA-256(Z65(P)) |
A.1, A.2, the classical form of D.2 |
| SHA-256 over the 32-byte x-coordinate | K = SHA-256(X32(P)) |
A.3 (and, through HKDF rather than SHA-256, the classical half of D.5) |
| Keccak-256 over the 65-byte point | K = Keccak-256(Z65(P)) |
A.4, A.5, A.6 |
| Keccak-256 over the 32-byte x-coordinate | K = Keccak-256(X32(P)) |
A.7 (legacy read path; pending frozen-vector verification) |
For the same pair of test keys (0x11…11, 0x22…22):
X32 = 77e0510d5042e2f5e9e59c977b81eeed590cf7d20c1c51da451a8eaa9fdc45ff
Z65 = 04 77e0510d…dc45ff 5bc2344d3d5d8376b78519f5077117b8c34f19870cd4be70f6517cd91f573738
SHA-256(Z65) = 4c68aa69740926aab5ca201a53db5d5b6d7e2f586ab1b0439db47cebddc2f924
SHA-256(X32) = fd264454c8f37c9c4b000f0672399b9c76011de71f489fd8a043e25e558226f9
4.5 Plaintext encoding of wrapped keys. A 32-byte key is wrapped in one of three encodings, and each entry states which:
- raw: the 32 key bytes;
- hex text:
utf8(hex0x(key)), a 66-byte ASCII string ("0x"plus 64 lower-case hex digits), produced from key bytes; - key text as supplied: the UTF-8 bytes of the key string that the caller supplied, taken
verbatim. In normal use this is
"0x"plus 64 lower-case hex digits (66 bytes), but it is not normalised: a string without0x, or in upper case, is encrypted as given. A consumer returns the decrypted text unchanged.
4.6 Hex parsing. Producers MUST emit well-formed lower-case hexadecimal. Consumers should reject malformed hex (odd length or non-hexadecimal characters). Not every shipping consumer checks this, so interoperability rests on producers emitting well-formed hex.
4.7 JSON. Field order shown in wire formats is the order the producer emits. Consumers parse JSON and do not depend on order, except where an entry states that exact JSON bytes are hashed, MACed or used as AAD.
4.8 Randomness in vectors. IVs, nonces, content keys and ephemeral keys are random at production
time. The vectors record the values that were produced; they are valid known answers for the consume
direction. Values that depend only on fixed test keys (derived keys, shared secrets, signatures from
deterministic signers, public keys from seeds) are reproducible from the inputs alone. Each entry in
the vectors file has an inputs object that states, for every sub-vector, the keys and the expected
plaintext.
4.9 Statuses.
- current — used by a Rootz product build.
- current (pre-release) — in a private or pre-release package, not yet in a released product build.
- exported-unused — available as a library function in a Rootz package, with no product data path that uses it.
- legacy-read-only — retained so that existing data can be read; not used for new data.
- reserved — an identifier reserved for a construction not yet specified.
Part A — secp256k1 ECDH key wraps
A.1 rootz.wrap.ecdh-secp256k1-sha256.v1 — device and member key wrap
Generic name. ECDH(secp256k1) key wrap; key = SHA-256 over the uncompressed shared point; AES-256-GCM.
Status. current.
Composition of. secp256k1 ECDH (SEC 1 §3.3.1, SEC 2); SHA-256 (FIPS 180-4); AES-256-GCM (FIPS 197,
SP 800-38D). Ephemeral forms: SEC 1 §5.1 ECIES structure; static member form: SP 800-56A §6.3
C(0e, 2s) structure. Deviations: the KDF is a single SHA-256 over Z65 (the whole point, not the
x-coordinate) with no counter or shared info; GCM replaces the separate encryption and MAC steps.
Purpose. Deliver a 32-byte master key (or other 32-byte key) to one recipient device or team member, so that only the holder of the recipient's secp256k1 private key can recover it.
Inputs and encodings.
MK: 32 bytes. Wrapped raw (Section 4.5).- Recipient public key
Q_R: 65-byte uncompressed (0x04 ‖ X ‖ Y). Producers reject other forms. - Sender key: either a fresh ephemeral key pair
(e, E)(the device, single-device and package forms), or the sender's long-term key(d_S, Q_S)(the member wrap, static-static). - KDF input:
Z65(Section 4.3), not the x-coordinate.
Produce.
1. Choose the sender key: (e, E) ← fresh secp256k1 key pair [ephemeral forms]
or (d_S, Q_S) [static member wrap]
2. P = e·Q_R (or d_S·Q_R)
3. K = SHA-256( Z65(P) ) # 32 bytes; no salt, no info
4. IV = rand(12)
5. (C, T) = GCM-Enc(K, IV, MK, A = empty) # len(C) = 32, len(T) = 16
6. Emit the wire structure below (tag split).
Consume.
1. P = d_R · E (ephemeral forms: E = ephemeralPub) or d_R · Q_S (member wrap)
2. K = SHA-256( Z65(P) )
3. MK = GCM-Dec(K, iv, ciphertext, A = empty, tag = authTag) # fails closed on tag mismatch
Where the sender key is found.
- Ephemeral forms: in the
ephemeralPubfield next to the encrypted key object. - Member wrap: the encrypted key object carries no sender key.
Q_Sis the 65-byte public key of the party that made the wrap (the secret's owner); the consumer obtains it from the context in which it received the wrap. [code-read]
Wire format. All values 0x-hex, lower case. Tag split.
Encrypted key object (member wrap, and inside every entry below):
{ "ciphertext": "0x…32 bytes…", "iv": "0x…12 bytes…", "authTag": "0x…16 bytes…" }
Device key entry (ephemeral form):
{ "deviceAddress": "0x…20-byte address…", "deviceName": "…",
"ephemeralPub": "0x04…65 bytes…",
"encryptedMasterKey": { "ciphertext": "0x…", "iv": "0x…", "authTag": "0x…" } }
deviceAddress may be the empty string. deviceName is optional.
Package (one entry per device, each with its own fresh ephemeral key):
{ "type": "fat-key-package", "version": "3.0",
"secretContract": "0x…", "keyVaultBlock": <int>, "secretName": "…", "chainId": <int>,
"timestamp": <ms since epoch>, "devices": [ <device key entry>, … ] }
Single-device package (fields in producer order):
{ "type": "single-device-package", "version": "1.0",
"secretContract": "0x…", "deviceAddress": "0x…",
"ephemeralPub": "0x04…65 bytes…",
"encryptedMasterKey": { "ciphertext": "0x…", "iv": "0x…", "authTag": "0x…" },
"algorithm": "ECDH-AES-256-GCM", "timestamp": <ms since epoch> }
The type also declares an optional encryptedKeyVaultKey (an encrypted key object under the same
ephemeral key); current producers do not emit it.
Vault form: consumers of vault data also accept the four fields in one object,
encryptedMasterKey: { ciphertext, iv, authTag, ephemeralPub }.
A second, different wire format also labelled
fat-key-package3.0exists; see A.5. The two are not interchangeable and are told apart by their fields (A.5).
Known-answer vectors (SPEC-v1.2-vectors.json → rootz.wrap.ecdh-secp256k1-sha256.v1).
Member wrap (static-static), owner d_S = 0x11…11, member d_R = 0x22…22,
Q_S = pub(0x11…11) (field owner_public_key):
MK a5a5…a5 (32 bytes)
Z65 0477e0510d5042e2f5e9e59c977b81eeed590cf7d20c1c51da451a8eaa9fdc45ff
5bc2344d3d5d8376b78519f5077117b8c34f19870cd4be70f6517cd91f573738
K 4c68aa69740926aab5ca201a53db5d5b6d7e2f586ab1b0439db47cebddc2f924
iv b16c7bd0a547229e2673a79e
ciphertext cce9b3419b6bc1e906fdd2dd18b150f608af60baca5eb0e3f1475bb9e86c7b7c
authTag 001d7836e5b6431dd7d0943ab9db97ac
Device wrap (ephemeral), recipient d_R = 0x22…22, deviceAddress = address of pub(0x22…22):
deviceAddress 0x1563915e194D8CfBA1943570603F7606A3115508
ephemeralPub 049516b4fa03baa622babbe9631372fb441d3c12155019d1f1387eb3fe7ee09a7f
d61d08abde04931004cf9fd6a59f2fae0c2447f49f70ab9c5fbcfbb1ccb6e039
iv 016089113b57c96a5d8b664f
ciphertext 091de3795e4676de711fa74817822b2581156e905288c5fb99a72e8a7420c49d
authTag 37c8bb1f93daa710b1a2e61cc627b60d → MK = a5a5…a5
The vectors file also holds a single-device package (recipient 0x22…22), a two-device package
(recipients 0x22…22 and 0x23…23) and a reverse vector (independent producer, ephemeral key
0x31…31, IV 0a0a…0a, plaintext a5a5…a5).
Security properties.
- Confidentiality of
MKagainst anyone without the recipient private key, under the hardness of ECDH on secp256k1; integrity of the wrapped key by the GCM tag. - The ephemeral forms give a fresh
Kper wrap.
Known limitations.
- No salt, info string or AAD. The wrap is not bound to the recipient identity, the device address,
the secret, the vault block or the package;
deviceAddress,deviceNameand the package fields are not authenticated. - Member wrap: every wrap between the same two keys uses the same
K; IV uniqueness rests on random 96-bit IVs; there is no forward secrecy. - The sender is not authenticated by this construction.
A.2 rootz.wrap.ecdh-secp256k1-sha256.team-invite.v1 — team-invite wrap
Generic name. ECDH(secp256k1) key wrap; key = SHA-256 over the uncompressed shared point; AES-256-GCM with appended tag.
Status. legacy-read-only.
Composition of. secp256k1 ECDH (SEC 1, SEC 2); SHA-256 (FIPS 180-4); AES-256-GCM (FIPS 197, SP 800-38D). Static-static, SP 800-56A §6.3 C(0e, 2s) structure, with the A.1 KDF deviation.
Purpose. Encrypt key material between the two parties of a team invitation.
Inputs and encodings.
- The caller supplies the shared value
Z65(the 65-byte uncompressed shared point between the two parties' keys, Section 4.3) as 0x-hex. - Plaintext: given as a hex string; the raw bytes it denotes are encrypted (for a 32-byte key, raw encoding).
Produce.
1. Z = Z65( d_A · Q_B ) # = Z65( d_B · Q_A )
2. K = SHA-256(Z)
3. IV = rand(12)
4. (C, T) = GCM-Enc(K, IV, plaintext_bytes, A = empty)
5. Emit { encrypted: hex0x(C ‖ T), iv: hex0x(IV), algorithm: "AES-256-GCM-ECDH" } # tag appended
Consume. Recompute Z and K; split encrypted into C (all but the last 16 bytes) and T
(last 16 bytes); GCM-decrypt.
Wire format.
{ "encrypted": "0x…C‖T…", "iv": "0x…12 bytes…", "algorithm": "AES-256-GCM-ECDH" }
Owner-self read path. The web dashboard also reads { "encrypted", "iv" } objects (same KDF, tag
appended, algorithm optional) in which an owner wrapped a key to itself, i.e. Z = Z65(d · pub(d)).
[code-read]
Known-answer vector. d_A = 0x11…11, d_B = 0x22…22, plaintext a5a5…a5 (32 bytes).
Z65 (identical to A.1 Z65)
K 4c68aa69740926aab5ca201a53db5d5b6d7e2f586ab1b0439db47cebddc2f924
iv 13cb0e3fde3214ae71a8dd1a
encrypted 09b64c3eaa3e074a07b75fc324aefa9dfda183ca7737b6e2c260a5868d4b24bf
f926ea66c8710079e98f82167b6a3b2a (32 + 16 bytes)
Known limitations. As A.1: no salt, info or AAD; a fixed key per pair of parties, with IV uniqueness resting on random 96-bit IVs. The envelope differs from A.1 (tag appended, different field names), so the two wire formats are not interchangeable although the key is the same.
A.3 epistery.wrap.ecdh-secp256k1-x-sha256.peer-cipher.v1 — peer-cipher wrap and content
Generic name. Static-static ECDH(secp256k1) key wrap; key = SHA-256 over the x-coordinate; AES-256-GCM. AES-256-GCM content encryption.
Status. current (pre-release). Dependency: epistery. The construction belongs to the Epistery library, a Rootz Corp project. It is specified here because Rootz Sealed Memory uses it (D.5 classical seats, D.6 content layout). Other Epistery constructions will be specified in version 1.3.
Composition of. secp256k1 ECDH with the SEC 1 / SP 800-56A x-coordinate shared secret; SHA-256
(FIPS 180-4); AES-256-GCM (FIPS 197, SP 800-38D). SP 800-56A §6.3 C(0e, 2s) structure. Deviation: the
KDF is a single SHA-256 over X32 with no counter or shared info.
Purpose. (a) Wrap a 32-byte content key from one identity key to another; (b) encrypt text content under a 32-byte content key.
Inputs and encodings.
- KDF input:
X32— the 32-byte x-coordinate, not the 65-byte point. This is the one difference in the KDF from A.1, and it makes the two incompatible: an A.1 wrap made with the same two keys does not open here (verified). - Wrapped key: 32 bytes, raw.
- Content plaintext:
utf8(text).
Produce — key wrap.
1. X = X32( d_wrapper · Q_peer ) # static-static; no ephemeral key
2. K_pair = SHA-256(X)
3. IV = rand(12)
4. (C, T) = GCM-Enc(K_pair, IV, K_content, A = empty)
5. Emit { ciphertext: hex0x(C), iv: hex0x(IV), tag: hex0x(T) } # tag split
Produce — content.
1. IV = rand(12)
2. (C, T) = GCM-Enc(K_content, IV, utf8(text), A = empty)
3. Emit { iv: hex0x(IV), ciphertext: hex0x(C ‖ T) } # tag appended
Consume. Wrap: X = X32(d_peer · Q_wrapper), K_pair = SHA-256(X), GCM-decrypt with the split
tag. Content: GCM-decrypt with the appended tag; decode UTF-8.
Wire format. 0x-hex, lower case.
wrap: { "ciphertext": "0x…32 bytes…", "iv": "0x…12…", "tag": "0x…16…" }
content: { "iv": "0x…12…", "ciphertext": "0x…C‖T…" }
When this wrap is carried in a seat (D.5), the object also has "alg": "epistery/peer-cipher:secp256k1-ecdh".
Known-answer vectors. Epistery's own frozen vectors of 2026-09-30, opened independently.
wrapper public key 044f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa
385b6b1b8ead809ca67454d9683fcf2ba03456d6fe2c4abe2b07f0fbdbb2f1c1 (= pub(0x11…11))
peer private key 0x22…22
K_pair = SHA-256(X) fd264454c8f37c9c4b000f0672399b9c76011de71f489fd8a043e25e558226f9
wrap ciphertext 30b7c8bb9b7e56249ebbe0fa4022c1b9279443ef9afcf3a90f2f5e289fa1227c
iv 84705ece6ea5ee22d5319248
tag fbe54e1be5468c1cf40360b90436d69c → K_content = abab…ab (32 bytes)
content iv ea00c799d12f1db94af4e0ea
content ciphertext 0b77d07111badc4de47cda1a0f9e2f604436a1ec161f77da674d4b8ffbae0672
5354406c9ad773a6de09bc63665ef2f075f8fdd51995e12fa87e18745b7fe3b9
f68b3f52f5573313da7aeb
→ "frozen vector — the existing construction, 2026-09-30 ✓" (UTF-8)
The vectors file also holds fresh_wrap, made by the shipping producer with the same key pair
(wrapper 0x11…11, peer 0x22…22) and the same content key abab…ab.
Known limitations.
- Static-static:
K_pairis constant for a pair of identity keys; safety rests on random 96-bit IVs (collision risk becomes material near 2^32 wraps per pair); no forward secrecy. - No salt, info or AAD; the wrap is not bound to the content it protects or to any record identifier.
- The content form has no AAD; it is not bound to a record name or metadata (see D.6).
A.4 rootz.wrap.ecdh-secp256k1-keccak256.teams.v1 — team key share
Generic name. ECDH(secp256k1) key wrap; key = Keccak-256 over the uncompressed shared point; AES-256-GCM with appended tag; key-text plaintext.
Status. current.
Composition of. secp256k1 ECDH (SEC 1, SEC 2); Keccak-256 (FIPS 202 §5.2 KECCAK[512], see
Section 3); AES-256-GCM (FIPS 197, SP 800-38D). SEC 1 §5.1 ECIES structure when the sender key is
ephemeral. Deviations: single Keccak-256 over Z65 with no counter or shared info; GCM in place of
encryption and MAC.
Purpose. Deliver a team secret's master key and vault key to a team member or device.
Inputs and encodings.
- KDF input:
Z65(65-byte point). KDF: Keccak-256 (Section 3). SHA3-256 does not open these wraps, nor do SHA-256(Z65) or Keccak-256(X32) (verified). - Plaintext: the key as key text as supplied (Section 4.5), normally
utf8(hex0x(MK)), 66 bytes. (Contrast A.5, which wraps the same kind of key raw.) - Sender key: a secp256k1 key pair, normally ephemeral. Its public key travels in the enclosing message (below).
Produce.
1. P = d_sender · Q_member
2. K = Keccak-256( Z65(P) )
3. IV = rand(12)
4. (C, T) = GCM-Enc(K, IV, utf8(keyText), A = empty) # normally 66-byte plaintext
5. Emit the JSON text { "encrypted": hex0x(C ‖ T), "iv": hex0x(IV) } # tag appended
Consume. P = d_member · Q_sender; K = Keccak-256(Z65(P)); GCM-decrypt (appended tag); the
plaintext is the key text, returned unchanged.
Wire format. The wrapped-key object:
{"encrypted":"0x…(66+16 bytes)…","iv":"0x…12 bytes…"}
Enclosing messages. The wrapped-key layout occurs in three messages. Only the fields this specification needs are shown; the messages carry further non-cryptographic fields.
- Key-delivery message (
"type": "encrypted-keys","version": "4.0"):encryptedMasterKeyandencryptedKVKeyare each the wrapped-key object serialised as a JSON string;encryptedKVKeymay benull.ephemeralPublicKeyis the sender's 65-byte uncompressed public key (0x04…).encryptionMethodis"ephemeral-ecdh-v4". Both keys are wrapped under the same sender key, and therefore the sameK, with independent random IVs. The sender key of this message is freshly generated for it; it is not a key carried by any earlier message of the invitation exchange. - Device-sync package (
"type": "bootstrap-sync","version": "1.0"): each element ofownedSecrets[]andteamShares[]carriesencryptedMasterKeyand optionalencryptedKVKeyas wrapped-key objects (not strings). One top-levelephemeralPublicKey(65-byte uncompressed) serves every wrapped key in the package. [code-read for the message assembly; the wrapped-key objects in the vectors were produced by the shipping code] - Direct key-share message (
"type": "direct-keys"). Two versions are produced.- Version
"4.0"(web dashboard).senderPublicKeyis the sender's 65-byte uncompressed public key, freshly generated for the message. The wrapped-key object is carried flat:encryptedMasterKeyis itsencryptedvalue (0x-hexC ‖ T) andivis itsivvalue, both top-level strings. The plaintext is the master key as key text as supplied. [code-read for the message assembly; the wrap in the vector was produced by the shipping key-sharing code] - Version
"6.0"(SDK). The same flatsenderPublicKey,encryptedMasterKeyandivas version 4.0, plus:ownerPublicKey— the sender's long-term 65-byte public key, for contact purposes; it is not the key-agreement input;encryptedMasterKeyFull— the object{ "encrypted", "iv", "algorithm": "AES-256-GCM-ECDH" }, which repeats the flat master-key wrap;encryptedKVKey— an object of the same form. When the secret has no distinct vault key, it repeats the master-key wrap (as in the vector). Some producers omit it and emit a booleanhasKvKey. - Algorithm label. The string
"AES-256-GCM-ECDH"in a direct-keys 6.0 message is the same string that A.2 emits, but the construction is this entry (A.4):K = Keccak-256(Z65). The A.2 key derivation,SHA-256(Z65), does not open these wraps (verified). Consumers MUST select the construction by message type and version, not by thealgorithmlabel. - A consumer obtains the master key from
senderPublicKey,encryptedMasterKeyandiv, and the vault key by the C.2 rule. The shipping web consumer reads only these three fields. [code-read]
- Version
Known-answer vectors. Member d = 0x22…22, sender d = 0x33…33, plaintext utf8("0xa5…a5").
sender public key 043c72addb4fdf09af94f0c94d7fe92a386a7e70cf8a1d85916386bb2535c7b1b1
3b306b0fe085665d8fc1b28ae1676cd3ad6e08eaeda225fe38d0da4de55703e0
Z65 049110f8760a37d96052e3dcaf14862a147654f49f722cf213568ccef1eca2ec71
34ebc7978f91470a8abcdd13332db0de3261055c06ac8ff92b75b272463c6adc
K daf9383c748ed84aa4aaa757036539fac3c4b3c8ceca6b7fccc8e986da268205
package {"encrypted":"0xab8d33d89bfc33231278a08bbf28ba8ed3a170ef2ebcb0bb0dba7aaee1f9d86588e0
ee775fe5af75b83e4773fa7f666e6bc854129efedcacfb6cc43487e6cc88622dcde6c97858d093fff43eb6
2a6872d59f","iv":"0x0d1c2c29bc316f46ce82507d"}
→ plaintext "0xa5a5…a5" (66 ASCII bytes)
The vectors file also holds: a reverse package (independent producer, same sender 0x33…33); a
key-delivery message and a device-sync package (sender 0x36…36, plaintexts "0xa5…a5" for the master
key and "0x77…77" for the vault key); and a direct-keys 4.0 message and a direct-keys 6.0 message
(recipient 0x22…22, random sender keys as recorded, plaintext "0xa5…a5"). The 6.0 message was
produced end to end by the shipping SDK send path, with the transaction captured and not sent.
Known limitations.
- No salt, info or AAD; not bound to team, secret or member identity.
- One producer interface returns the sender's ephemeral private key to its caller. Forward secrecy of such a wrap depends on the caller discarding that key.
- Where a sender key is reused,
Kis fixed for the pair and safety rests on random IVs. - Same KDF as A.5 and A.6 but a different plaintext encoding (key text vs raw); not interchangeable.
A.5 rootz.wrap.ecdh-secp256k1-keccak256.identity.v1 — multi-device identity package
Generic name. ECDH(secp256k1) multi-recipient key wrap; key = Keccak-256 over the uncompressed shared point; AES-256-GCM.
Status. current.
Composition of. secp256k1 ECDH (SEC 1, SEC 2); Keccak-256 (FIPS 202 §5.2 KECCAK[512]);
AES-256-GCM (FIPS 197, SP 800-38D). SEC 1 §5.1 ECIES structure, one ephemeral key per recipient.
Deviations as A.4.
Purpose. Deliver an identity's master key and vault key to each of its authorised devices.
Inputs and encodings.
- KDF input:
Z65. KDF: Keccak-256 (as A.4). - Plaintexts: master key
MKand vault keyKVK, each 32 bytes, raw. - Package ephemeral public key: producers emit the 33-byte compressed form (
0x02/0x03 ‖ X). Consumers accept both the compressed and the 65-byte uncompressed form (SEC 1 §2.3.4);Z65is computed from the full shared point in either case. - The direct single wrap may use the sender's static key or an ephemeral key; the construction is the same.
Produce (package, per device).
1. (e, E) ← fresh secp256k1 key pair
2. K = Keccak-256( Z65(e · Q_device) )
3. (C1,T1) = GCM-Enc(K, IV1 = rand(12), MK, A = empty)
4. (C2,T2) = GCM-Enc(K, IV2 = rand(12), KVK, A = empty) # same K, second IV
5. Emit the device entry with ephemeralPublicKey = compressed(E) # tag split
Consume. K = Keccak-256(Z65(d_device · E)); GCM-decrypt each object (split tag).
Wire format. 0x-hex.
{ "type": "fat-key-package", "version": "3.0",
"secretContract": "0x…", "identityContract": "0x…", "secretName": "…",
"devices": [ { "deviceAddress": "0x…", "deviceName": "…",
"ephemeralPublicKey": "0x02…|0x03… (33 bytes)",
"encryptedMasterKey": { "ciphertext": "0x…", "iv": "0x…", "authTag": "0x…" },
"encryptedKeyVaultKey": { "ciphertext": "0x…", "iv": "0x…", "authTag": "0x…" } } ],
"createdAt": <ms since epoch> }
Single wrap: { "ciphertext", "iv", "authTag" }.
Distinguishing A.5 from the A.1 package. Both are labelled fat-key-package / 3.0. They differ
in the KDF (Keccak-256 vs SHA-256), the ephemeral-key field name (ephemeralPublicKey vs
ephemeralPub), its encoding (33-byte compressed vs 65-byte uncompressed), the timestamp field
(createdAt vs timestamp) and the vault-key field. A consumer tells them apart by these fields; the
type/version labels do not distinguish them.
Known-answer vectors. Device d = 0x22…22, sender d = 0x33…33 (single wrap):
Z65, K identical to A.4 (same key pair)
K daf9383c748ed84aa4aaa757036539fac3c4b3c8ceca6b7fccc8e986da268205
iv ab8dcb319ba2a5efb935d60c
ciphertext 498218e26a2781c7aa8201f98fc057e77cb04a9e086a427e88b8ad04e5016fc4
authTag 394e9b73059ecde62aa9283d4d8017fa → MK = a5a5…a5 (raw)
Package entry, device d = 0x22…22:
ephemeralPublicKey 02590f2df040946f6c840ee23d630471f7ebe8382599507248a8deb9931ca9bd69
encryptedMasterKey ct a014faca6a7f7b22819b068d25dae7f0bf2c36407e22f1f4d3d2cdbc56a0d618
iv f94fba9db8f0b5f9b9e333ce tag 3b8564437df0930edbe7d4dd8ebf04fb → a5a5…a5
encryptedKeyVaultKey ct 187d80caf1dbf4f5c25a56c3736764d63046ada7c654ccaf943f4524c62c98c7
iv 20f495754da352df0807d45b tag c43841d4bba0f99f0bd1ef8efc1701a6 → 7777…77
The reverse vector (independent producer, ephemeral 0x34…34, plaintext a5a5…a5) carries a 65-byte
uncompressed ephemeral key, which the shipping consumer accepts.
Known limitations.
- No salt, info or AAD.
MKandKVKare encrypted under the same key with independent random IVs, and neither ciphertext is bound to its role. - Package metadata (
secretContract,identityContract, device address and name) is not authenticated.
A.6 rootz.wrap.ecdh-secp256k1-keccak256.identity-device-key.v1 — identity device key package
Generic name. ECDH(secp256k1) key wrap; key = Keccak-256 over the uncompressed shared point; AES-256-GCM with appended tag; raw plaintext.
Status. exported-unused.
Composition of. secp256k1 ECDH (SEC 1, SEC 2); Keccak-256 (FIPS 202 §5.2 KECCAK[512]);
AES-256-GCM (FIPS 197, SP 800-38D). Static-static or ECIES structure depending on the sender key the
caller supplies; deviations as A.4.
Purpose. Encrypt a master key for one device of an identity, and for every device of an identity that has a registered public key.
Inputs and encodings. KDF K = Keccak-256(Z65(P)) (the same key as A.4 and A.5 for the same pair
of keys). Plaintext: the 32 key bytes, raw. Sender key: supplied by the caller; its 65-byte
uncompressed public key is carried in the package.
Produce.
1. P = d_sender · Q_recipient
2. K = Keccak-256( Z65(P) )
3. IV = rand(12); (C, T) = GCM-Enc(K, IV, MK, A = empty)
4. Emit { recipientAddress, encryptedKey: hex0x(C ‖ T), iv: hex0x(IV), senderPublicKey: hex0x(Q_sender) }
Consume. K = Keccak-256(Z65(d_recipient · senderPublicKey)); split the last 16 bytes of
encryptedKey as the tag; GCM-decrypt; the result is the raw key.
Wire format.
{ "recipientAddress": "0x…", "encryptedKey": "0x…(32+16 bytes)…", "iv": "0x…12…",
"senderPublicKey": "0x04…65 bytes…" }
The multi-device form is an array of these objects, one per device.
Known-answer vector. Recipient d = 0x22…22, sender d = 0x33…33, MK = a5a5…a5.
K daf9383c748ed84aa4aaa757036539fac3c4b3c8ceca6b7fccc8e986da268205 (= A.4 K)
recipientAddress 0x1563915e194D8CfBA1943570603F7606A3115508
iv ff967359fe6f13e8ec7d0625
encryptedKey c90217fd1d85867c17c47a840e3840cba6b777f5d551f94b08c200b3bd627ac0
bfde97dc54be87c725144577527f65c3 → MK = a5a5…a5 (raw)
senderPublicKey = pub(0x33…33) (see A.4)
Known limitations. No salt, info or AAD; recipientAddress is not authenticated. Where the
sender uses a static key, K is fixed for the pair and safety rests on random IVs.
A.7 rootz.wrap.ecdh-secp256k1-keccak256-x.legacy.v5-2 — legacy Keccak-256(X32) family
Generic name. ECDH(secp256k1) key wrap; key = Keccak-256 over the x-coordinate; AES-256-GCM.
Status. legacy-read-only. (A frozen vector from an existing record is pending; see below.)
Composition of. secp256k1 ECDH with the x-coordinate shared secret (SEC 1, SP 800-56A);
Keccak-256 (FIPS 202 §5.2 KECCAK[512]); AES-256-GCM (FIPS 197, SP 800-38D).
Description. Records written by an earlier generation of Rootz software may have derived the wrap
key as K = Keccak-256(X32(P)), where an earlier version of the elliptic-curve library returned the
32-byte x-coordinate as the shared secret. The envelopes are expected to be those of A.4 and A.5. This
identifier is reserved so that such records can be described exactly. The formula, envelopes and a
known-answer vector will be stated in a revision of this document once a frozen vector from an
existing record has been verified. Until then this entry makes no byte-level claim beyond the
formula above.
Part B — Signature-derived storage keys
These constructions derive a symmetric key from a deterministic signature over a fixed message. They differ in the message, the address case, and what is hashed: the signature's hex text (B.1) or its raw bytes (B.2).
Common signature procedure: EIP-191 personal_sign over utf8(message), i.e. ECDSA on secp256k1
over Keccak-256(utf8("\x19Ethereum Signed Message:\n") ‖ utf8(decimal(len(utf8(message)))) ‖ utf8(message)), where the length is the byte length of the UTF-8 encoding of the message, written
in decimal ASCII. The nonce is RFC 6979 deterministic, s is normalised to the low half of the group
order, and the result is 65 bytes r(32) ‖ s(32) ‖ v(1) with v ∈ {27, 28}.
B.1 rootz.wrap.sig-storage-key.sha256-hextext.v1 — owner at-rest master key
Generic name. Key derived by SHA-256 over the hex text of a deterministic ECDSA(secp256k1) signature; AES-256-GCM with appended tag.
Status. current.
Composition of. Deterministic ECDSA on secp256k1 (RFC 6979, EIP-191 framing; used as secret KDF input); SHA-256 (FIPS 180-4); AES-256-GCM (FIPS 197, SP 800-38D).
Purpose. Store a secret's master key encrypted for its owner, recoverable by any device that can produce the owner's signature.
Inputs and encodings.
- Message (three lines, LF separators, no trailing newline), with the owner's address in EIP-55
mixed case:
Storage key for master key encryption Owner: <EIP-55 address> Purpose: encrypt-master-key sig= 65-byte signature as above.sigText = hex0x(sig)— 132 ASCII characters, lower-case hex.- KDF:
K = SHA-256(utf8(sigText))— the hash is over the 132-byte hex text. SHA-256 over the 65 raw bytes does not open these packages (verified). - Plaintext: the master key as key text as supplied (Section 4.5), normally 66 bytes.
Produce.
1. msg = "Storage key for master key encryption\nOwner: " ‖ EIP55(addr) ‖ "\nPurpose: encrypt-master-key"
2. sig = EIP191_Sign_RFC6979(d_owner, msg) # 65 bytes, v = 27/28
3. K = SHA-256( utf8( hex0x(sig) ) )
4. (C, T) = GCM-Enc(K, IV = rand(12), utf8(keyText), A = empty)
5. Emit the package with encryptedKey = hex0x(C ‖ T) # tag appended
Consume. Re-sign the same message with the owner key, derive K, GCM-decrypt, return the text.
Wire format.
{ "version": "1.0.0", "algorithm": "AES-256-GCM", "keyDerivation": "signature-storage-key",
"encryptedKey": "0x…(66+16 bytes)…", "iv": "0x…12…",
"encryptedFor": "<EIP-55 address>", "timestamp": <ms>,
"extensions": { … application metadata … } }
Known-answer vector. Owner d = 0x11…11.
address 0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A
message "Storage key for master key encryption\nOwner: 0x19E7E376E7C213B7E7e7e46cc70A5dD086DAff2A\nPurpose: encrypt-master-key"
signature 0xdab7809ff42444ec19ed311495bb121a99c80f12eacf70ea127aa68d7b3a910e
16fd54ecc2cd66e23daced6d362aad1d6da9b6896573b0dc8d2b98dd5505972c1b
K 69a5936f246239dd0fccf86a14b2f54ffc467ec1d76023242229836acdc5782c
iv 317588ca25d8926d97a7aea3
encryptedKey db875186f97a7233a0a92f7984f375cfc3941daa0de5e951bfaf5533655f3a7d
0870523fdc02885427576b170241a02425647b2f335b40daf51a61a010aed3d7
77bb6e44fb81428236da9c9508c8dbab53cf → "0xa5a5…a5" (66 ASCII bytes)
Security properties. The key is exactly as strong as the secrecy of the owner's signing key.
Known limitations.
- Correctness depends on a deterministic signer and on the exact text form. A signer that randomises
ECDSA nonces, encodes
vdifferently (for example as 0/1) or emits upper-case hex derives a differentK, and the master key cannot be recovered with it. - The message is constant per address: one
Kper owner for all secrets; random IVs only. No AAD; package fields andextensionsare not authenticated.
B.2 rootz.wrap.sig-storage-key.sha256-rawsig.hardware-keys.v1 — device key at rest under a hardware-wallet signature
Generic name. Key derived by SHA-256 over the raw bytes of a deterministic ECDSA(secp256k1) signature; AES-256-GCM.
Status. current.
Composition of. Deterministic ECDSA on secp256k1 (RFC 6979, EIP-191 framing; used as secret KDF input); SHA-256 (FIPS 180-4); AES-256-GCM (FIPS 197, SP 800-38D).
Purpose. Keep a device's secp256k1 ECDH private key encrypted at rest, unlockable only by a signature from the user's hardware wallet.
Inputs and encodings.
- Message. Two registered message texts exist; each record carries its message in the field
signatureMessage, and a consumer signs the message exactly as carried. All lines use LF separators, with no trailing newline, and the wallet address in lower case.- Local record (eight lines):
Rootz Device Key Encryption This signature unlocks your encryption keys. Only sign this on trusted devices. Wallet: <lower-case 0x address> Domain: rootz.global Version: 1 - On-chain record (nine lines;
<chainId>in decimal):Rootz Device Key Encryption This signature protects your device encryption keys. Only sign this on trusted devices. Wallet: <lower-case 0x address> Chain: <chainId> Domain: rootz.global Version: 1
- Local record (eight lines):
- KDF:
K = SHA-256(sig)over the 65 raw signature bytes. The hex-text reading of B.1 does not open these records (verified). - Plaintext: the device ECDH private key, 32 bytes, raw.
Produce.
1. (d_dev, Q_dev) ← fresh secp256k1 key pair
2. sig = EIP191_Sign_RFC6979(d_wallet, msg)
3. K = SHA-256(sig) # 65 bytes in
4. (C, T) = GCM-Enc(K, IV = rand(12), d_dev, A = empty)
5. Emit the record (tag split)
Consume. Have the wallet sign signatureMessage; K = SHA-256(sig); GCM-decrypt; optionally check
that d_dev yields publicKey.
Wire format.
Local record:
{ "version": "1.0", "publicKey": "0x04…(device ECDH public key, 65 bytes)…",
"deviceAddress": "<EIP-55 address of the hardware WALLET>",
"encryptedPrivateKey": { "ciphertext": "0x…32…", "iv": "0x…12…", "authTag": "0x…16…" },
"signatureMessage": "<the local message>", "createdAt": <ms> }
On-chain record: the bytes utf8("ROOTZ_DEVICE_KEY") ‖ utf8(JSON), where JSON is
{ "version": "1.0", "type": "rootz-device-key",
"walletAddress": "<EIP-55 wallet address>",
"publicKey": "0x04…65 bytes…",
"encryptedPrivateKey": { "ciphertext": "0x…32…", "iv": "0x…12…", "authTag": "0x…16…" },
"signatureMessage": "<the on-chain message>", "createdAt": <ms>, "chainId": <int> }
Consumers also accept the JSON without the prefix.
Known-answer vectors. Wallet d = 0x11…11.
message "Rootz Device Key Encryption\n\nThis signature unlocks your encryption keys.\nOnly sign this on trusted devices.\n\nWallet: 0x19e7e376e7c213b7e7e7e46cc70a5dd086daff2a\nDomain: rootz.global\nVersion: 1"
signature 0x73d739205f5db0409d356827c1d9756ff708c643a91cf8854109e1c6cf8ac680
5aae46f9552367de901e5a1a71e2456bcac8bf996af3a572e3b4f46f7d30c5281b
K 7a1c4acc75fa798ecfbdd4e0fe0d407158fb0b59b9dd28165a94155d8a861a36
publicKey 047c4af3d5a93a67a370579aedbeaf5b473ef9f5836c6a5bc15ef66e2f4315c4e1
26915615c521f92fb5e787f68941c3278aaa728b5dce90b5302924464be187ac
iv a4c9765e4af0a4fb25d08b3d
ciphertext 30920e94c3c80aca8a277cab47b4db6a5b4adbd4d49d03b89f93cb0101a91cda
authTag a60a105d651ee49cd5f25a3cb5bb6996 → d_dev with pub(d_dev) = publicKey
The vectors file also holds a reverse vector (independent producer, device key 0x66…66) and an
on-chain record for chain 137 produced by the shipping code (field onchain_record).
Security properties. As B.1: depends on a deterministic signer and on the secrecy of the wallet key.
Known limitations. One K per wallet and message; no AAD. The unlocked device key is the
recipient key for Part A wraps, so its compromise exposes every key wrapped to that device.
Part C — Content encryption
C.1 rootz.content.aes-256-gcm.v1 — content, bytes and content-identifier encryption
Generic name. AES-256-GCM content encryption, appended tag.
Status. current.
Composition of. AES-256-GCM (FIPS 197, SP 800-38D); SHA-256 (FIPS 180-4) for the optional string-to-key helper.
Purpose. Encrypt secret content, raw bytes, and IPFS content identifiers under a 32-byte key (normally the secret's master key).
Inputs and encodings. Key: 32 bytes (given as 0x-hex). Text: utf8(text). IPFS CID: utf8(cid)
of its string form. Optional helper: a key derived from a string is SHA-256(utf8(s)).
Produce.
IV = rand(12); (C, T) = GCM-Enc(K, IV, plaintext, A = empty); field = hex0x(C ‖ T) # tag appended
Consume. Split the last 16 bytes of the field as the tag; GCM-decrypt.
Wire format.
text: { "encrypted": "0x…C‖T…", "iv": "0x…12…", "algorithm": "AES-256-GCM" }
IPFS CID: { "encrypted": "0x…C‖T…", "iv": "0x…12…", "version": "5.0" }
(Byte form: the same C ‖ T and IV as byte arrays.)
Known-answer vectors. Key 4242…42 (32 bytes).
text "Rootz KAT — content ✓"
iv 74d97336f93481bcb771532e
encrypted e270efe211cfc9e10bd454e8f521f6ffcf980c696404fcc2efbe6853a5c61387874fff9b2176601b0c
bytes 00010203feff
iv 624236b33ad8612a4c5afbcc
encrypted 1d9a9cb383c4b0567fa7b6de622d90069911d418edeb
CID "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG"
iv 8924c3cc1fa04f652e333445
encrypted ca660f12fddfe0b508a93e6aa1b199944e2c51bc95642d4812f202b5635114ae
32bd39fc3d8593359d1a4b7c557ff13424fc5cbe55c06c640af6902677e4
Security properties. Confidentiality and integrity of each ciphertext under GCM.
Known limitations. No AAD: a ciphertext is not bound to the record, contract or position it belongs to. Many ciphertexts share one key; IV uniqueness rests on random 96-bit IVs. The string-to-key helper is a single unsalted hash, suitable only for high-entropy strings.
C.2 rootz.content.note.kvkey-aes-256-gcm.v1 — notes, files and the vault key
Generic name. AES-256-GCM note, file and key encryption under a vault key.
Status. current.
Composition of. AES-256-GCM (FIPS 197, SP 800-38D), applied as in C.1.
Purpose. Encrypt notes written to a vault, the files they reference, and the vault key itself.
Inputs and encodings.
- Master key
MK(32 bytes) and vault keyKVK(32 bytes). - KVK wrap plaintext: hex text,
utf8(hex0x(KVK)), 66 bytes, produced from the key bytes and encrypted underMK. - Note payload: the UTF-8 JSON text of an object with fields, in producer order:
version("1.0.0"),content,author(lower-case address),timestamp,files,noteVersion,previousNote,metadata. The JSON is not canonicalised. - Each
files[i]carries its IPFS CID encrypted underKVKin the C.1 CID form asipfsRef: { encrypted, iv, version: "5.0" }, plus plain fields such asfilename,contentType,size,uploadedAt. - File content: the plaintext is the base64 text (RFC 4648) of the file bytes, i.e.
utf8(base64(file)), encrypted underKVK. - Which vault key applies. If the vault data carries a wrapped vault key,
KVKis obtained by the KVK unwrap below. If it does not, the web dashboard and the agent package use the 32 master-key bytes themselves asKVK; the agent runtime container uses C.3 instead. [code-read]
Produce.
KVK wrap: (C,T) = GCM-Enc(MK, rand(12), utf8(hex0x(KVK))) → {encrypted: hex0x(C‖T), iv, algorithm:"AES-256-GCM"}
Each file: ipfsRef = C.1-CID-encrypt(KVK, cid); content: GCM-Enc(KVK, rand(12), utf8(base64(bytes)))
Note: IV = rand(12); (C,T) = GCM-Enc(KVK, IV, utf8(JSON(payload)))
On-chain: bytes = IV(12) ‖ C ‖ T(16)
Consume. KVK: GCM-decrypt under MK, parse the 66-character hex. Note: take bytes [0:12] as IV
and the rest as C ‖ T; GCM-decrypt under KVK; parse JSON; decrypt each ipfsRef under KVK.
File: GCM-decrypt, then base64-decode.
Wire format. KVK wrap: as C.1 text form. Note on-chain: raw bytes IV ‖ C ‖ T.
Known-answer vectors. MK = a5a5…a5, KVK = 7777…77.
KVK wrap iv 611f9f2221b56435a3b408be
encrypted f848371c9c97ff42024dc852f457b8ad15a13f0141a11519817c684ae78b92d1
5e675107c5856fe8279ef7a3e187d38458eb90a6b427f21029e2b771a3c2c01f
166ef67a31e90d5d101a6224c74679685f54 → "0x7777…77" (66 ASCII bytes)
file bytes 00ff0a0dc8; iv 54effd3d6218d802196ef8bd
encrypted 37d995fc7e807d1700c3135682a4f33375aec5b41a5a344d → "AP8KDcg=" (base64 text)
note iv 586053f184ae7b99fce1ed76; full on-chain bytes in the vectors file
→ {"version":"1.0.0","content":"hello note","author":"0xabcdef0000000000000000000000000000000001",
"timestamp":1792149128272,"files":[{"ipfsRef":{"encrypted":"0xcf0d…9072","iv":"0x072d349d628f876745558cb6",
"version":"5.0"},"filename":"a.txt","contentType":"text/plain","size":3,"uploadedAt":1792149128272}],
"noteVersion":"2","previousNote":7,"metadata":{"k":"v"}}
Known limitations. As C.1. No AAD anywhere: a note ciphertext is not bound to its contract,
vault block or note index. One KVK encrypts every note, CID and file in a vault; IV uniqueness rests
on random IVs. Base64 encoding before encryption expands file ciphertexts by about one third.
C.3 rootz.kdf.kvkey-keccak256-mktext.v1 — vault key derived from the master-key text
Generic name. Vault key derived by Keccak-256 over the master-key text.
Status. current (pre-release).
Composition of. Keccak-256 (FIPS 202 §5.2 KECCAK[512]); the derived key is used with C.1/C.2
(AES-256-GCM, FIPS 197, SP 800-38D).
Purpose. Provide a vault key for a secret whose vault data carries no wrapped vault key
(encryptedKVKey absent), in the agent runtime.
Formula.
KVK = Keccak-256( utf8(MKtext) )
MKtext is the master key as text, exactly as recovered by the A.1 unwrap: "0x" followed by 64
lower-case hex digits (66 bytes). The 32-byte KVK is then used as the vault key of C.2.
Where the vault data does carry a wrapped vault key, the vault key is obtained by the C.2 KVK unwrap instead. For vault data without a wrapped vault key, other Rootz components use the master-key bytes themselves as the vault key (C.2); the two keys differ, so data written under one is not readable under the other. [code-read for where each rule applies; the formula was evaluated with the same library function the agent runtime calls]
Known-answer vector.
MKtext "0xa5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5"
KVK 87271b7931cbfe875457a579e08c4d846c0ec6205aa38036ed928a8e85710d7f
sample C.1 text form under KVK: iv f86c1e0390c234a6831cf6c5,
encrypted dacab210d448b8ed49076e5e6be11f64e3bce2ff41b4a3f2ccc30b57b6 → "agent-tee KAT"
Related standard format. The agent runtime also reads its own signing key from an Ethereum keystore file in the standard Web3 Secret Storage format (version 3), through a library. That format is cited by reference and is not a Rootz construction; it is not re-specified here.
Known limitations. The vault key is a deterministic, unsalted function of the master key; it provides no separation between master-key and vault-key holders.
Part D — Post-quantum and hybrid constructions
D.1 rootz.envelope.pq.mlkem1024-hkdfsha384-aesgcm.v2 — post-quantum envelope v2
Generic name. Multi-recipient ML-KEM-1024 envelope: HKDF-SHA-384 key-wrap key, AES-256-GCM DEK wrap, AES-256-GCM body with the header as AAD.
Status. exported-unused.
Composition of. ML-KEM-1024 (FIPS 203), used as SP 800-227 describes (the KEM shared secret is input to a KDF and the derived key protects a data key); HKDF-SHA-384 (RFC 5869, FIPS 198-1, FIPS 180-4); AES-256-GCM (FIPS 197, SP 800-38D). A header signature uses ML-DSA-65 (FIPS 204); it is an integrity function, described only for interoperability.
Purpose. Encrypt a payload once, under a random data-encryption key (DEK), for one or more recipients holding ML-KEM-1024 keys.
Inputs and encodings.
- Recipient ML-KEM-1024 key pairs. Key generation from a 64-byte seed uses FIPS 203
ML-KEM.KeyGen_internal(d, z)withseed = d(32) ‖ z(32). Public key 1568 bytes; ciphertext 1568 bytes; shared secret 32 bytes. recipientId,senderId: strings. Both are KDF inputs.- Where
senderIdand the sender key are found. The envelope carries neithersenderIdnor the sender's ML-DSA-65 public key. The consumer obtains both from the context in which the envelope was received; the application that conveys the envelope binds them (for example, a sender identifier and its published key). The consumer also needs its ownrecipientIdand its own ML-KEM public key (an HKDF info input). - Plaintext: raw bytes.
Produce.
1. DEK = rand(32); n = rand(12) # body nonce
2. For each recipient i:
(ct_i, ss_i) = ML-KEM-1024.Encaps(pk_i)
info_i = utf8("rootz-dek-wrap-v2|" ‖ recipientId_i ‖ "|" ‖ senderId) ‖ pk_i # pk_i raw, 1568 bytes
WK_i = HKDF-SHA384(IKM = ss_i, salt = SHA-384(ct_i), info = info_i, L = 32)
w_i = rand(12); (Cw, Tw) = GCM-Enc(WK_i, w_i, DEK, A = empty)
wrappedDek_i = w_i ‖ Cw ‖ Tw # 60 bytes
3. canonical = the exact UTF-8 bytes of the JSON text
{"alg":A,"mv":MV,"n":N,"r":[{"alg":RA_1},…,{"alg":RA_k}],"rwb":RWB,"v":V}
with keys in this order and no whitespace, where
A = the envelope "algorithm" (JSON string)
MV = the envelope "minVersion" (JSON number)
N = hex(n), lower case (JSON string)
RA_i = recipient i's "algorithm" (JSON string); one object per recipient and no other field
RWB = the reWrapBy string (JSON string; "" when there is none)
V = the envelope "version" (JSON number)
Strings are escaped as ECMAScript JSON.stringify does.
4. (C, T) = GCM-Enc(DEK, n, plaintext, A = canonical)
5. headerSignature = ML-DSA-65.Sign(sk_sender, canonical ‖ T) # pure, empty context
6. Emit the JSON below
Producers always emit reWrapBy, as a JSON string, defaulting to "".
Consume.
1. Rebuild canonical from the received fields exactly as in Produce step 3, applying the rules:
- reWrapBy absent, or null, false or 0 → RWB = ""
- N = hex(unhex(nonce)), re-encoded in lower case
- V and MV are the received "version" and "minVersion", parsed as JSON numbers and written
as ECMAScript JSON.stringify writes a Number: an integer in decimal, with no sign, fraction,
exponent or leading zeros (a received 2.0 is written 2). Producers emit integers.
2. Reject if version < 2.
3. Verify headerSignature over canonical ‖ unhex(tag) with the sender's ML-DSA-65 public key.
Verification is mandatory and precedes decryption: on failure, reject.
4. Find the own recipient entry by id; ss = ML-KEM-1024.Decaps(sk, unhex(encapsulation))
5. WK = HKDF-SHA384(ss, salt = SHA-384(unhex(encapsulation)), info as above with own id, senderId, own pk)
6. DEK = GCM-Dec(WK, wrappedDek[0:12], wrappedDek[12:44], tag = wrappedDek[44:60])
7. plaintext = GCM-Dec(DEK, unhex(nonce), unhex(ciphertext), A = canonical, tag = unhex(tag))
Wire format. JSON; all binary values lower-case hex without 0x.
{ "version": 2, "minVersion": 2, "algorithm": "ML-KEM-1024+AES-256-GCM",
"nonce": "<hex 12>", "reWrapBy": "<string>",
"recipients": [ { "id": "<recipientId>", "epoch": <int>, "algorithm": "ML-KEM-1024",
"encapsulation": "<hex 1568>", "wrappedDek": "<hex 60>" } ],
"headerSignature": "<hex 3309>", "ciphertext": "<hex>", "tag": "<hex 16>" }
Known-answer vector. KEM seed d ‖ z = 4444…44 (64 bytes); sender signing master seed 5555…55
(the ML-DSA-65 key pair is ML-DSA.KeyGen_internal(ξ) of FIPS 204 (Algorithm 6, ML-DSA-65
parameters), with ξ = HKDF-SHA384(IKM = seed, salt = empty, info = utf8("rootz-signing-v2"), L = 32));
recipientId = "device-A", senderId = "sender-1", epoch = 3, reWrapBy = "2030-01-01", plaintext
"PQ envelope KAT plaintext". The vectors file carries sender_id, recipient_id, the full recipient
ML-KEM public key, the full sender ML-DSA public key and the exact canonical header.
SHA-256(recipient ML-KEM pk) 882f9299da0551ca9d8929fc95c222b1fa137c2447d9e1779d64ea953e950439
SHA-256(sender ML-DSA pk) d7d75b6db43bbc00f218a2ebdd39a19fc468a4d723f5024a835af7807f41efec
canonical {"alg":"ML-KEM-1024+AES-256-GCM","mv":2,"n":"60fc4155966c780116743907","r":[{"alg":"ML-KEM-1024"}],"rwb":"2030-01-01","v":2}
ss 520287900fe65fc5e8be5fc770f5b70e57908d2d7e9e303ab1c1ae75474f71be
WK 303cc78db3d713b3713d6ed9060c0e046b5656d6ee836bb795b8a129ce6fe51d
wrappedDek e8543c757347bfda37e4f37b8ef8bf6c813db77668a8288fb2590e8135fd01dc
3892ca3546d94613d8f0d61ff5b965a737bb0b95c48285c0bc380ec8
DEK 7d4da5270755c7de84da3b4ee38f78ce1163bdb83974b026cad0e16fab689fbe
nonce 60fc4155966c780116743907
ciphertext 6ce96982994f75ed44d36a0e835b7d218b4cfd64bafa8c5ddc
tag 1b7899c7831f906767ff5619d0ee25cb
Interoperability note. info is 1,603 bytes for one recipient. Some HKDF interfaces cap info
at 1,024 bytes; an implementation on such an interface computes RFC 5869 directly from
HMAC-SHA-384.
Security properties. Payload confidentiality under ML-KEM-1024 and AES-256-GCM. The wrap key binds
the ML-KEM ciphertext (salt), the recipient's public key and both identifiers (info). The header
signature and the AAD cover the algorithm, versions, nonce, reWrapBy and the number of recipient
entries with their algorithm labels.
Known limitations.
- The canonical header does not contain the recipient entries'
id,epoch,encapsulationorwrappedDek. The recipientepochfield is not covered by the AAD or by the signature. A recipient's ownidandencapsulationare bound to its wrap through the KDF and the GCM tag ofwrappedDek; entries of other recipients are not authenticated to a recipient. minVersionis covered by the header, but see D.2: the agile dispatcher does not enforce it.reWrapByis an opaque string; the vectors cover a date string and the empty string only.
D.2 rootz.envelope.agile.v1 — algorithm-tagged envelope
Generic name. Algorithm-tagged envelope. Classical form: ephemeral ECDH(secp256k1)/SHA-256 wrap of a content key, with AES-256-GCM content encryption. Post-quantum form: D.1.
Status. exported-unused.
Composition of. Classical form: secp256k1 ECDH, SHA-256 and AES-256-GCM exactly as the A.1 ephemeral form (SEC 1 §5.1 structure, A.1 deviations), with AES-256-GCM (FIPS 197, SP 800-38D) for the content. Post-quantum form: D.1.
Purpose. One sealing interface whose output states its own algorithm, so that a consumer can dispatch.
Algorithm identifiers (complete list). "ECDH-AES-256-GCM" (version 1, classical, default) and
"ML-KEM-1024" (version 2, produces a D.1 envelope). Any other identifier is rejected at seal time.
("ML-KEM-1024+X25519" is declared but has no seal or open route.)
Produce (classical).
1. CEK = rand(32); IV = rand(12)
2. (C, T) = GCM-Enc(CEK, IV, plaintext, A = empty)
3. wrap = A.1 ephemeral device entry wrapping CEK (raw) to the recipient's secp256k1 key
4. Emit { v: 1, alg: "ECDH-AES-256-GCM", ct: base64(C ‖ T), iv: base64(IV), wrap }
Consume (dispatch).
1. If the JSON has a numeric top-level "version" ≥ 2 → open as D.1.
2. Else if alg == "ECDH-AES-256-GCM" → unwrap CEK per A.1 from wrap.ephemeralPub /
wrap.encryptedMasterKey; GCM-decrypt base64-decoded ct (tag appended) with base64-decoded iv.
3. Else → error.
A string-valued "version" (for example "2") does not select step 1.
Wire format (classical). ct and iv are standard base64 (RFC 4648 §4); wrap fields are 0x-hex
as A.1 (deviceAddress may be empty or absent).
{ "v": 1, "alg": "ECDH-AES-256-GCM", "ct": "<base64 C‖T>", "iv": "<base64 12 bytes>",
"wrap": { "deviceAddress": "", "ephemeralPub": "0x04…",
"encryptedMasterKey": { "ciphertext": "0x…", "iv": "0x…", "authTag": "0x…" } } }
Known-answer vector. Recipient d = 0x22…22; plaintext "agile classical payload".
{"v":1,"alg":"ECDH-AES-256-GCM","ct":"6c0Cm40ABJfgX0roBG4tSN6mNPB1ZkJaO9ZHAwASSceXUpShRWIY",
"iv":"wKk7tpONXpNrKcXD","wrap":{"deviceAddress":"",
"ephemeralPub":"0x04f397df702d54a470f19176057d33f58be90e6b448170f84a9472f5959528fbdb11581637b9cef4560e060190f8c17b2b06167f3307ee89645e56229b0bb35117",
"encryptedMasterKey":{"ciphertext":"0x6c14cc7b0736943dee4a6508343fb7f6206ae8c399128f4dbc3ea7c1ff7c4a3a",
"iv":"0x8acce034b834dcd32b2ffb41","authTag":"0x8a8293d3deb74e0dbea9874ad4ebabd9"}}}
The vectors file also holds a reverse envelope (independent producer, ephemeral key 0x35…35,
plaintext "indep agile").
Known limitations.
- Classical form: the properties and limitations of A.1 (ephemeral form). No AAD binds
v/algto the ciphertext. - The dispatcher does not enforce a minimum version; the minimum version of 2 applies only inside the D.1 path.
D.3 rootz.kem.hybrid.mlkem1024-x25519-sha3.v1 — hybrid KEM combiner
Generic name. Hybrid KEM combiner: ML-KEM-1024 and X25519 shared secrets combined by SHA3-256 together with both ciphertexts and both recipient public keys.
Status. exported-unused.
Composition of. ML-KEM-1024 (FIPS 203); X25519 (RFC 7748); SHA3-256 (FIPS 202 §6.1). The combined
input is a concatenation of shared secrets in the sense of SP 800-56C Rev. 2 §2 (Z′ = Z ‖ T),
extended with the ciphertexts and public keys.
Purpose. Combine a post-quantum and a classical shared secret into one 32-byte secret.
Produce (encapsulate).
1. (ct_M, ss_M) = ML-KEM-1024.Encaps(pk_M)
2. (e_X, E_X) ← fresh X25519 key pair; ss_X = X25519(e_X, pk_X)
3. ss = SHA3-256( utf8("rootz-pq-wallet-v2") ‖ ss_M(32) ‖ ss_X(32) ‖ ct_M(1568)
‖ E_X(32) ‖ pk_X(32) ‖ pk_M(1568) )
4. Output (ct_M, E_X, ss)
Consume (decapsulate). ss_M = Decaps(sk_M, ct_M), ss_X = X25519(sk_X, E_X), then step 3. Field
order is significant; all fields are fixed length and there are no length prefixes.
Wire format. None defined (function outputs only).
Known-answer vector. Recipient X25519 secret 8888…88; recipient ML-KEM seed 4444…44 (d ‖ z).
The vectors file carries the full ML-KEM ciphertext ct_M (1568 bytes).
recipient X25519 pk 30d3c865a48fceb3d6118577cf2e5f228d6ff69866264757785b253cb7a4806a
ephemeral X25519 pk 6da41d7c39ab682bfaed66dd5685a90de875098ae5b1d22b6efb37aa30f53d2b
ct_M 73a41b60f3f391512716d2fe5ff0b9b8… (1568 bytes, in the vectors file)
ss 2f20a702582398ddefe21522e558484f51bb448150f404fbe04d01966192978d
Security properties. The combined secret depends on both shared secrets, both ciphertexts and both recipient public keys. No deployed data path uses this construction.
D.4 rootz.keychain.mlkem1024-epochs-hkdfsha384.v2 — epoch key chain
Generic name. ML-KEM-1024 epoch key sequence derived with HKDF-SHA-384; backward AES-256-GCM wrap of the previous decapsulation key; SHA-384 chain hash.
Status. exported-unused.
Composition of. HKDF-SHA-384 (RFC 5869); ML-KEM-1024 key generation (FIPS 203); AES-256-GCM (FIPS 197, SP 800-38D); SHA-384 (FIPS 180-4).
Purpose. Derive a sequence of ML-KEM-1024 key pairs ("epochs") from one master seed, and let the
holder of epoch n recover the decapsulation key of epoch n−1.
Produce.
seed_n = HKDF-SHA384(IKM = master, salt = empty, info = utf8("rootz-enc-epoch-" ‖ decimal(n)), L = 64)
(pk_n, sk_n) = ML-KEM-1024.KeyGen_internal(d = seed_n[0:32], z = seed_n[32:64])
# sk_n is the FIPS 203 decapsulation key: dk_PKE(1536) ‖ ek(1568) ‖ H(ek)(32) ‖ z(32) = 3168 bytes
For n ≥ 1:
WK_n = HKDF-SHA384(IKM = sk_n[0:32], salt = empty, info = utf8("rootz-key-chain-wrap-v2"), L = 32)
prevKeyWrapped_n = nonce(12) ‖ C ‖ T where (C,T) = GCM-Enc(WK_n, nonce = rand(12), sk_{n-1}, A = empty)
# 12 + 3168 + 16 = 3196 bytes
Chain hash (integrity): leaf_n = SHA-384(0x00 ‖ pk_n ‖ prevKeyWrapped_n) (prevKeyWrapped empty for n = 0)
h_0 = leaf_0; h_n = SHA-384(0x01 ‖ h_{n-1} ‖ leaf_n)
Consume. From sk_n, compute WK_n, split prevKeyWrapped_n, GCM-decrypt to obtain sk_{n-1}.
Wire format. No serialised link format is defined. A chain link exists in memory only, as the
structure { epoch (integer), pubkey (1568 bytes), prevKeyWrapped (3196 bytes, or none for epoch 0), keyChainHash (48 bytes) }. It is not an interchange format.
Known-answer vector. Master seed 9999…99 (32 bytes). The vectors file shows each link's fields
as hex, for testing only.
epoch 0 SHA-256(pk) f79759e2e1fcb16fddd1aaa32df605b8a9835e5086a4cb85fe5addac2f6ed6ae
keyChainHash f2f029d837e475ba3ac59b1543fa9f44bdcc2c2d5f76d0e80954d16cd5da4431
0287622a85d1f8cb9e338f18eb74ff66
epoch 1 SHA-256(pk) 362382667c918b123e695ac12246f6c648a5a042a763c269ed6087e9f03cbcf0
prevKeyWrapped 9360701462bc484deeac246e… (3196 bytes) → sk_0
keyChainHash 9456663c4d8b409ed31024cc2d6ea861a6d3fe9bc7f3e6336eacf333aafe1129
0ca0301b9632732a21c3ce7fdc232e21
epoch 2 SHA-256(pk) 49004cdb77416b605e22cbb2e39af6d52c5be7c20ee9b368213c4f338f7c711d
prevKeyWrapped b04653227d0eb417fcb860ff… (3196 bytes) → sk_1
keyChainHash 09107ad1a60d9eaeeda2206e824501c8594e5d08252e9afc0e88aba0362d7274
d78252fde4d4745a083559f0f51851c3
Known limitations.
- Anyone holding the master seed holds every epoch; holding epoch
ngives every earlier epoch. - The wrap key's input is
sk_n[0:32], the first 32 bytes of the encoded secret polynomial vector: secret material, but neither a seed nor a uniformly random string. HKDF is used as an extractor over it. The same info label is used for every epoch, with no salt. - No AAD: a link is not bound to its epoch number or public key by the encryption (the chain hash is a separate integrity value).
D.5 rootz.wrap.hybrid.secp256k1-mlkem1024-hkdfsha256.sealed-memory.v1 — hybrid seat wrap
Generic name. Hybrid key wrap: static-static ECDH(secp256k1) x-coordinate concatenated with an ML-KEM-1024 shared secret, HKDF-SHA-256, AES-256-GCM; carried in a seat object.
Status. current (pre-release).
Composition of. secp256k1 ECDH with the x-coordinate shared secret (SEC 1, SP 800-56A); ML-KEM-1024
(FIPS 203, used as SP 800-227 describes); the hybrid shared secret Z′ = Z ‖ T of SP 800-56C Rev. 2 §2
with the two-step (extraction-then-expansion) derivation of SP 800-56C Rev. 2 §5, instantiated as
HKDF-SHA-256 (RFC 5869); AES-256-GCM (FIPS 197, SP 800-38D).
Purpose. Wrap a 32-byte record key K from a granting identity to a member, so that recovery
needs both the member's secp256k1 private key and its ML-KEM-1024 decapsulation key.
Inputs and encodings.
- Classical half: static-static ECDH between the granter's and the member's secp256k1 identity
keys, output
X32(32-byte x-coordinate), notZ65(the 65-byte reading does not open these wraps; verified). There is no ephemeral classical key. - ML-KEM-1024 encapsulation to the member's ML-KEM public key. The member's key pair is generated from
a 64-byte seed as in D.1:
ML-KEM.KeyGen_internal(d = seed[0:32], z = seed[32:64]). K: 32 bytes, raw.
Produce.
1. X = X32( d_granter · Q_member )
2. (ct_M, ss_M) = ML-KEM-1024.Encaps(pk_M_member)
3. KEK = HKDF-SHA256(IKM = X ‖ ss_M, salt = empty, info = utf8("sealed-memory/v1/hybrid:secp256k1-ecdh+ml-kem-1024"), L = 32)
# empty salt = 32 zero bytes per RFC 5869 §2.2
4. (C, T) = GCM-Enc(KEK, IV = rand(12), K, A = empty)
5. Emit { alg, kemCipherText: hex0x(ct_M), iv: hex0x(IV), ciphertext: hex0x(C), tag: hex0x(T) } # tag split
Consume. X = X32(d_member · Q_granter) with Q_granter from the seat's granterPublicKey;
ss_M = Decaps(sk_M, kemCipherText); derive KEK; GCM-decrypt.
Wire format — the wrap. 0x-hex.
{ "alg": "hybrid:secp256k1-ecdh+ml-kem-1024", "kemCipherText": "0x…1568 bytes…",
"iv": "0x…12…", "ciphertext": "0x…32…", "tag": "0x…16…" }
Wire format — the seat. A wrap is carried in a seat object (fields in producer order):
{ "seatFormat": 1,
"memberPublicKey": "0x04…65 bytes…",
"granterPublicKey": "0x04…65 bytes…",
"label": <string|null>,
"grantedAt": "<ISO 8601 date-time>",
"wrap": <wrap object> }
granterPublicKeycarriesQ_granter, the classical-half public key a consumer needs.wrap.algselects the wrap construction:"hybrid:secp256k1-ecdh+ml-kem-1024"— this entry;"epistery/peer-cipher:secp256k1-ecdh"— a classical seat: the wrap is an A.3 key wrap{ alg, ciphertext, iv, tag }from the granter's key to the member's key.- A seat whose
wraphas noalgfield is a classical seat (A.3). [code-read] - Any other
algvalue is rejected; there is no fallback. [code-read]
- A seat whose
seatFormatis not1is rejected. [code-read] - A seat with
wrap: null(andgranterPublicKey: null,grantedAt: null) is a pending seat with no key to open. labelis a human hint and is not used in any computation. The member's ML-KEM public key is not carried in the seat.
Known-answer vectors. Granter 0x11…11, member 0x22…22, member ML-KEM seed 4545…45 (64 bytes),
K = abab…ab.
X 77e0510d5042e2f5e9e59c977b81eeed590cf7d20c1c51da451a8eaa9fdc45ff
ss_M 14e30fa8c28eb6b48da5d6bd6b8c52835a0b3d9e8ae4db7543a6517b188c5fad
KEK accd233485308ef700f7a62b01e2517bbe51f3d0be06d7018ff75887b8cf8c72
iv f62b553e93fe575d5c03ed75
ciphertext abd6cb27aa2431133d2f5ccaa00f82593eb883171f17139b5d54bd76db288a6b
tag 7c107d3bd961febcd8a2aa722bfd57ff
kemCipherText: in the vectors file
The vectors file also holds a reverse wrap (independent producer), and a hybrid seat and a classical seat produced by the shipping code with the same keys. Each seat opens from its own fields.
Security properties. Recovering K requires both halves (classical and post-quantum).
Known limitations.
- The KDF does not bind the ML-KEM ciphertext or either public key, and there is no AAD: the wrap is not transcript-bound.
- The classical half is static-static (fixed per granter/member pair); freshness of
KEKcomes only from the ML-KEM half. - Seat fields (
memberPublicKey,label,grantedAt) are not authenticated by the wrap.
D.6 rootz.seal.sealed-memory.content-embedding-hmac-naming.v2 — record seal and record naming
Generic name. AES-256-GCM record encryption with an HKDF-SHA-256 naming subkey and an HMAC-SHA-256 record name.
Status. current (pre-release).
Composition of. AES-256-GCM (FIPS 197, SP 800-38D); HKDF-SHA-256 (RFC 5869); HMAC-SHA-256 (FIPS 198-1). The content layout is the A.3 content form.
Purpose. Seal one memory record (its text and its embedding vector together) under a 32-byte
record key K, and give it a name that does not reveal its identifiers.
Produce.
1. payload = UTF-8 JSON of { title, content, embedding, tags, metadata, key, project }
# project = the record's coordinates.project (may be null)
2. IV = rand(12); (C, T) = GCM-Enc(K, IV, payload, A = empty)
3. N = HKDF-SHA256(IKM = K, salt = empty, info = utf8("sealed-memory/v2/naming"), L = 32)
4. recordName = hex( HMAC-SHA256(N, utf8( JSON.stringify([namespace, project, key]) )) )
# exact bytes: a JSON array of three values, no whitespace, e.g. ["ns","p1","k1"];
# strings are escaped as ECMAScript JSON.stringify does (non-ASCII emitted as UTF-8)
5. Emit the record below (content tag appended)
Consume. GCM-decrypt ciphertext (appended tag) with K and iv; parse JSON.
Wire format.
{ "sealFormat": 2, "contentWire": "epistery/peer-cipher:aes-256-gcm",
"recordName": "<hex 32 bytes>", "wrap": { "alg": "local" },
"iv": "0x…12…", "ciphertext": "0x…C‖T…",
"clear": { "id": "…", "namespace": "…", "kind": "…", "createdAt": <n>,
"contentHash": <…|null>, "actorId": <…|null>, "epoch": <int> } }
wrap.alg must be "local", the only accepted value in a sealFormat 2 record: K is held by the
reader and is not carried in the record. A record with any other wrap.alg is rejected.
[code-read] (Keys are delivered to other readers by seats, D.5, not by this field.) clear is
plaintext metadata.
Known-answer vector. K = cdcd…cd; namespace "ns", project "p1", key "k1".
naming key N b190f9b2eeeccbc2b26afcac01f2c0ea2d672e2bf61825c3b86e4ab11c2d58c9
recordName 62b2ef6a56a6bbcfbd1936a498c1072eeb2a92c4b77dce521f6ce460170a031d
iv 6f76de4ffa1a54a46d13fa6d
ciphertext 747d4e57b03022000da3bf389716d6238c2802197d749194a1cd393cb23b9caa
b46bb2ded94934acfaefa2750e2177c8bc3cf412345e50dbafa3583572d8f5e9
bf49eb6da6f51725bb91d343e510b7fb9fc9935b31af523c8e07a8a2f2f27fae
95c320c6322c1ff46e2d7b9c053f52f9048b7edb1ef1ea3c3f7dc719fd048cd2
a4f60bae69c413984dae
→ {"title":"T","content":"secret body","embedding":[0.125,-0.5,1],"tags":["a"],"metadata":{"m":1},"key":"k1","project":"p1"}
Security properties. Content and embedding are confidential together; the embedding is not exposed separately.
Known limitations.
- The ciphertext is not bound to its record: neither
recordNamenor anyclearfield is AAD. clearfields (namespace, kind, timestamps, epoch) are visible to anyone holding the record.
Part E — Other key-agreement and key-splitting constructions
E.1 rootz.seal.ecdh-p256-hkdf-sha256.proof-binder.v1 — sealed box (P-256)
Generic name. ECIES-style sealed box: ephemeral ECDH(P-256), HKDF-SHA-256, AES-256-GCM.
Status. current (pre-release).
Composition of. ECDH on NIST P-256 (FIPS 186-5, SP 800-186; SP 800-56A ECC CDH); HKDF-SHA-256 (RFC 5869); AES-256-GCM (FIPS 197, SP 800-38D). SEC 1 §5.1 ECIES structure. Deviations: HKDF-SHA-256 with fixed salt and info in place of the ANSI X9.63 KDF; GCM in place of encryption and MAC.
Purpose. Encrypt a message to the holder of a P-256 key (anonymous sender).
Produce.
1. (e, E) ← fresh P-256 key pair
2. Z = X32( e · Q_R ) # 32-byte x-coordinate
3. key = HKDF-SHA256(IKM = Z, salt = utf8("rootz-proof-salt"), info = utf8("rootz-proof-v1"), L = 32)
4. IV = rand(12); (C, T) = GCM-Enc(key, IV, plaintext, A = empty)
5. Emit { epk: hex(E uncompressed, 65 bytes), iv: hex(IV), ct: hex(C ‖ T) } # no 0x; tag appended
Consume. Z = X32(d_R · epk); derive key; GCM-decrypt.
Wire format. { "epk": "<hex 65>", "iv": "<hex 12>", "ct": "<hex C‖T>" } (lower-case hex, no
0x). Public keys are uncompressed X9.62 / SEC 1 hex.
Known-answer vector. Recipient d = 3333…33 (P-256).
recipient pub 0451a7580833898ea1b183cbd7350a4099078c6ef1c1e18e970cd7683035f25e7d
0110522712b0b5a7cff081685486984a94e6831edac46e7360fa9d834a7a81a1
epk 043e4fbf76c0fe40351b17e69f51c25f6a67d1218c8850679344071c58db77c85b
ccd03834723b968269a05fb13f63840563359fda386e6773c9e701a08abcaac6
key d83ea42388fc38a93177ab5614381cbea1a84cc00728ed7ca1df151aaf4d7a7b
iv aa596a58bdb24026b9f584c7
ct 8fba859af5d57b5537fd41e5a5e8728a2a2e9cc9df159713c41e9e92db847004483f335ecb
→ plaintext "sealed prompt — KAT" (UTF-8)
Security properties. A fresh key per box.
Known limitations. The sender is not authenticated inside the box (any authentication is a separate signature, out of scope). No AAD.
E.2 rootz.recovery.shamir-gf256-0x11b.v1 — Shamir secret sharing over GF(2^8)
Generic name. Shamir secret sharing over GF(2^8).
Status. exported-unused.
Composition of. Shamir secret sharing (1979) over GF(2^8) with reduction polynomial
x^8 + x^4 + x^3 + x + 1 (0x11b, the FIPS 197 field).
Purpose. Split a secret of any length into n shares, any k of which recover it.
Produce. For each secret byte s, choose a_1 … a_{k−1} uniformly at random and evaluate
a(x) = s + a_1·x + … + a_{k−1}·x^{k−1} in GF(2^8) at x = 1 … n. Share i is
{ index: i, data: [a_b(i) for each byte b] } (same length as the secret).
Consume. Lagrange interpolation at x = 0, byte by byte, over the supplied shares.
Wire format. { index: <1..n>, data: <bytes> } per share. The threshold k is not encoded.
Known-answer vector. Secret 00 ab…ab ff (32 bytes), 3-of-5:
1 f490edae42b8960317c72b249606b2ddc3b14481b2c65dfa29d0cc772c8bf73c
2 5387f0686c159a2d52771b3536323c8d84ae6cca36d8400fa9bc698a9333db14
3 a7bcb66d8506a785ee1b9bba0b9f25fbecb483e02fb5b65e2bc70e56141387d7
4 10806c3f069afea51c184ad63bcf3ce2246536bd00df16e0db2c04d76fcd46f1
5 e4bb2a3aef89c30da074ca59066225944c7fd99719b2e0b15957630be8ed1a32
Any three shares recover the secret.
Security properties. Information-theoretic secrecy below the threshold.
Known limitations. No below-threshold detection and no share integrity: combining fewer than k
shares, or a corrupted share, yields a different value without an error.
Part F — Local at-rest stores
F.1 rootz.atrest.agent-storage.keccak-privtext.v1 — agent local secret store
Generic name. At-rest store: key = Keccak-256 over the private-key text and a fixed label; AES-256-GCM with a 128-bit IV.
Status. current.
Composition of. Keccak-256 (FIPS 202 §5.2 KECCAK[512]); AES-256-GCM (FIPS 197, SP 800-38D) with
a 128-bit IV (SP 800-38D §7.1, non-96-bit IV case).
Purpose. Encrypt an automated agent's local store of secrets (including master keys) under a key derived from the agent's signing key.
Inputs and encodings.
K = Keccak-256(utf8(privText ‖ ":agent-storage-key")), whereprivTextis the agent's private key exactly as configured (in the vector,"0x"plus 64 lower-case hex digits). The same key without0x, or in upper case, gives a differentK(verified).- IV: 16 bytes (128 bits). GCM then derives its initial counter block by GHASH over the IV (SP 800-38D §7.1, non-96-bit case); consumers must support that path.
- Plaintext: UTF-8 JSON array of secret records (the producer pretty-prints it).
Produce. IV = rand(16); (C, T) = GCM-Enc(K, IV, utf8(JSON(records)), A = empty).
Consume. Derive K from the same privText; GCM-decrypt; parse JSON.
Wire format. { "iv": "<hex 16>", "encrypted": "<hex C>", "authTag": "<hex 16>" } — hex without
0x, tag split.
Known-answer vector. privText = "0x1212…12".
K baacf49261d89309eb68000beca584109261e89783a11f6811eaf9cc53387fe1
iv f4537b42447282a9e4ee09f31cdf5326
authTag 42a96739cf554a91019975d74cf94708
encrypted: in the vectors file → a JSON array with one record whose masterKey is "0xa5a5…a5"
Known limitations.
- The storage key is a deterministic function of the agent's signing key, with no salt and no work factor: confidentiality of the store is equivalent to confidentiality of that key.
":agent-storage-key"is a fixed domain separator, not a salt.- The 16-byte IV is valid GCM but not the recommended 96-bit form.
F.2 rootz.atrest.mcp-accumulator.aes-256-gcm.v1 — session transcript buffer
Generic name. Line-by-line AES-256-GCM buffer encryption with a sequence-prefixed IV.
Status. current.
Composition of. AES-256-GCM (FIPS 197, SP 800-38D).
Purpose. Encrypt, line by line, a local buffer of an AI-assistant session before it is archived.
Inputs and encodings. A random 32-byte key per session. Each line's plaintext is utf8(JSON text)
of one buffered event. IV = uint32_be(seq) ‖ rand(8) (12 bytes), where seq is the line's sequence
number.
Produce. (C, T) = GCM-Enc(K, IV, utf8(line), A = empty); emit
{ seq, enc: base64(IV ‖ C), tag: base64(T) }.
Consume. b = base64-decode(enc); IV = b[0:12], C = b[12:]; GCM-decrypt with
T = base64-decode(tag).
Wire format. { "seq": <int>, "enc": "<base64 IV‖C>", "tag": "<base64 16 bytes>" }, one per line.
Known-answer vector. K = 2121…21.
{"seq":7,"enc":"AAAAB4hn2WoWgEj0YiFe3H11RVMxlTjji+M/0DRNK2oxMcvv6TQSOTsOWAk73TI5ZgXxh7x8",
"tag":"WvWdFMHnqxCNWBKLlzyCsw=="}
→ {"ts":1,"type":"observation","text":"kat"}
Security properties. Per-line confidentiality and integrity.
Known limitations. Each line is protected on its own. There is no AAD, and the buffer as a whole has no integrity: the order and completeness of lines are not protected by this construction. Storage of the session key is key management and is out of scope (Section 1.3).
Part G — Hardware-bound key derivation
These constructions derive a secp256k1 private key that the Part A constructions then use as a recipient key to unwrap master keys. They are included because the derived key decrypts. The hardware operations were not executed for this document: each vector uses a software stand-in and is not hardware-verified.
G.1 rootz.hwkey.tpm-keccak-entropy-rsasig.v1 — TPM-bound wallet key
Generic name. Key derived by Keccak-256 over a software entropy value and a deterministic RSA PKCS#1 v1.5 signature over a stored label.
Status. current.
Composition of. RSASSA-PKCS1-v1_5 with SHA-256 (RFC 8017 §8.2; computed by a TPM-resident
RSA-2048 key, used as secret KDF input); Keccak-256 (FIPS 202 §5.2 KECCAK[512]).
Purpose. Derive a wallet key that can be recreated only with both a software secret and one specific TPM.
Formula.
entropy = rand(32) # software factor
label = the wallet's stored label (a parameter; see below)
sig = RSASSA-PKCS1-v1_5-Sign_SHA256(TPM RSA-2048 key, utf8(label)) # 256 bytes
priv = Keccak-256( entropy(32) ‖ sig(256) ) # 32-byte secp256k1 private key (big-endian)
The label is a stored parameter. Each wallet record stores its label (field salt) next to its
public metadata, and the label is re-read whenever the key is re-derived. Two values are registered:
| Label | Use |
|---|---|
rootz-tpm-wallet-v1 |
The desktop TPM wallet. |
rootz-tpm-signer-v1 |
The default of the TPM signer library interface when no label is supplied. |
Different labels give different keys. [code-read for where each label is used and stored; the derivation itself was executed with both labels]
Encoding details. The label is passed to the TPM as the hex of its UTF-8 bytes; the TPM signs the
UTF-8 bytes. The TPM returns the signature as hexadecimal text (upper case, no prefix). The
concatenation entropy ‖ sig is over the decoded bytes.
Known-answer vectors — SOFTWARE STAND-IN, NOT HARDWARE-VERIFIED. A generated RSA-2048 test key
replaces the TPM-resident key. Only its public key (SPKI DER) is published in the vectors file, with
the recorded signature for each label; the private key is not needed to check the derivation,
because priv depends only on the entropy and the signature. PKCS#1 v1.5 is deterministic, so each
label gives one signature.
entropy 0e0e…0e (32 bytes)
label rootz-tpm-wallet-v1 → signature 9A54B15563753628B710DA75AD817B98… (256 bytes, in the vectors file)
address 0x2E5EfD1765D80F9c4fc01503482ec59bf7Cf2BFE
label rootz-tpm-signer-v1 → signature A832AD6EF5E66DB3CE687E4AA415966E… (256 bytes, in the vectors file)
address 0xfB947F5f13C6508F21eEe66e834fa4b7d490133b
Known limitations.
- Requires a deterministic signature. A change to a randomised scheme (for example PSS) would change the key on every derivation.
- No domain separator in the Keccak input.
- Device binding, not runtime protection: the derived key exists in ordinary memory while in use. Anyone holding both the entropy and access to the same TPM key can recreate it.
G.2 rootz.hwkey.se-keccak-domain-entropy-ecdh-anchor.v1 — Secure-Enclave-bound wallet key
Generic name. Key derived by Keccak-256 over a fixed label, a software entropy value and an ECDH(P-256) x-coordinate with a fixed public key.
Status. current.
Composition of. ECDH on NIST P-256 (SP 800-56A ECC CDH; performed with a Secure-Enclave-resident
key); Keccak-256 (FIPS 202 §5.2 KECCAK[512]).
Purpose. As G.1, with an Apple Secure Enclave P-256 key in place of the TPM.
Formula.
entropy = rand(32)
S = X32( SE P-256 private key · A ) # 32-byte x-coordinate
priv = Keccak-256( utf8("rootz-se-wallet-v1") ‖ entropy(32) ‖ S(32) )
A is a fixed P-256 public key embedded in the application (a valid point, not the generator):
A = 04059c4056243314e6e6c668bcace85dd16305810f63ea1918df244baf9354a9c4
ae00304a6a7959d0e36b24347e110896ba344815d33c1da24fbb0ae2cd361303
The enclave helper returns S as the 32-byte x-coordinate (the CryptoKit shared secret), encoded as
0x plus lower-case hex. [code-read; not executed on hardware]
Known-answer vector — SOFTWARE STAND-IN, NOT HARDWARE-VERIFIED. The enclave key is replaced by the
software P-256 key 5e5e…5e.
S (X32) 4ca8d09c39c0600516d562653fde2b726a67143585a849c770aa9ca7b0c918b4
entropy 0e0e…0e
priv 7046782a5060e8f2aeb5c2f3830458f58716e7e652f481e7179dbbb4d0f5468f
address 0x536875A7d5A3b49872F6aC77B87dA5BA42E91df1
Known limitations.
- Any party that knows the discrete logarithm of
Aand the enclave public key can computeS; the secrecy ofprivthen rests on the entropy alone. The application states that the private key ofAwas discarded;Ais not a nothing-up-my-sleeve value. - Device binding, as G.1.
Appendix A — Identifier registry
This version specifies 24 constructions: 23 with known-answer vectors, and one (A.7) whose vector is pending.
Products: Desktop = Rootz Desktop (installer), which bundles the web dashboard, its MCP server
component and the SDK packages it uses; Web = the web dashboard code of the Rootz Layer, served to
browsers; SDK = the Rootz SDK packages (@rootz/*); Sealed Memory = Rootz Sealed Memory;
Proof binder = the Rootz proof binder; Agent TEE = the Rootz agent runtime container (a
pre-release component).
| § | Identifier | Status | Products | Packages and components |
|---|---|---|---|---|
| A.1 | rootz.wrap.ecdh-secp256k1-sha256.v1 |
current | SDK, Desktop, Web, Agent TEE | @rootz/crypto; byte-compatible copies in @rootz/hardware-keys (library and web page) and @rootz/team-invite (mock provider); agile classical wrap in @rootz/pq-crypto; read path in the agent runtime container |
| A.2 | rootz.wrap.ecdh-secp256k1-sha256.team-invite.v1 |
legacy-read-only | SDK, Web | @rootz/team-invite; owner-self read path in the web dashboard |
| A.3 | epistery.wrap.ecdh-secp256k1-x-sha256.peer-cipher.v1 |
current (pre-release) | Sealed Memory (dependency: epistery) | Epistery peer-cipher module, used by Rootz Sealed Memory (classical seats, content layout) |
| A.4 | rootz.wrap.ecdh-secp256k1-keccak256.teams.v1 |
current | SDK, Desktop, Web | @rootz/teams; callers in @rootz/api-server, @rootz/agent and the web dashboard |
| A.5 | rootz.wrap.ecdh-secp256k1-keccak256.identity.v1 |
current | SDK, Desktop, Web | @rootz/identity-provider |
| A.6 | rootz.wrap.ecdh-secp256k1-keccak256.identity-device-key.v1 |
exported-unused | SDK | @rootz/identity-provider |
| A.7 | rootz.wrap.ecdh-secp256k1-keccak256-x.legacy.v5-2 |
legacy-read-only | (data written by an earlier generation) | — |
| B.1 | rootz.wrap.sig-storage-key.sha256-hextext.v1 |
current | SDK, Desktop, Web | @rootz/crypto |
| B.2 | rootz.wrap.sig-storage-key.sha256-rawsig.hardware-keys.v1 |
current | SDK | @rootz/hardware-keys (library and web page) |
| C.1 | rootz.content.aes-256-gcm.v1 |
current | SDK, Desktop, Web, Agent TEE | @rootz/crypto; copy in @rootz/hardware-keys |
| C.2 | rootz.content.note.kvkey-aes-256-gcm.v1 |
current | SDK, Desktop, Web | @rootz/notes |
| C.3 | rootz.kdf.kvkey-keccak256-mktext.v1 |
current (pre-release) | Agent TEE | agent runtime container |
| D.1 | rootz.envelope.pq.mlkem1024-hkdfsha384-aesgcm.v2 |
exported-unused | SDK | @rootz/pq-crypto |
| D.2 | rootz.envelope.agile.v1 |
exported-unused | SDK | @rootz/pq-crypto |
| D.3 | rootz.kem.hybrid.mlkem1024-x25519-sha3.v1 |
exported-unused | SDK | @rootz/pq-crypto |
| D.4 | rootz.keychain.mlkem1024-epochs-hkdfsha384.v2 |
exported-unused | SDK | @rootz/pq-crypto |
| D.5 | rootz.wrap.hybrid.secp256k1-mlkem1024-hkdfsha256.sealed-memory.v1 |
current (pre-release) | Sealed Memory | Rootz Sealed Memory |
| D.6 | rootz.seal.sealed-memory.content-embedding-hmac-naming.v2 |
current (pre-release) | Sealed Memory | Rootz Sealed Memory |
| E.1 | rootz.seal.ecdh-p256-hkdf-sha256.proof-binder.v1 |
current (pre-release) | Proof binder | Rootz proof binder |
| E.2 | rootz.recovery.shamir-gf256-0x11b.v1 |
exported-unused | SDK | @rootz/pq-crypto |
| F.1 | rootz.atrest.agent-storage.keccak-privtext.v1 |
current | SDK | @rootz/agent |
| F.2 | rootz.atrest.mcp-accumulator.aes-256-gcm.v1 |
current | Desktop | Rootz Desktop (MCP server component) |
| G.1 | rootz.hwkey.tpm-keccak-entropy-rsasig.v1 |
current | Desktop, SDK | @rootz/tpm-provider; desktop wallet in the web dashboard |
| G.2 | rootz.hwkey.se-keccak-domain-entropy-ecdh-anchor.v1 |
current | Desktop, SDK | @rootz/secure-enclave-provider (and its native helper) |
Appendix B — How the vectors were produced and verified
B.1 The vectors file. SPEC-v1.2-vectors.json contains, under vectors.<identifier>, the
status, the generic_name, a vector object, and an inputs object that states the keys and
expected plaintext of every sub-vector. All private keys, seeds and content keys in it are test values
(repeated-byte patterns). For G.1 only the public half of the generated RSA test key is included. Hex strings are reproduced exactly as
they appear on the wire, with or without a 0x prefix.
B.2 How independence was established. Three separate pieces of work contribute, and they are described here as they were done.
- Producer. For every vector, the shipping Rootz implementation (its built distribution, or the
shipped module) produced the values from test keys. Vectors marked
reversewere produced by an independent implementation and opened by the shipping implementation. - Known-answer harness (written alongside version 1.0 of this specification). Its consumers
opened every producer output. They used node:crypto on OpenSSL 3.6 for AES-GCM, SHA-2, SHA-3, HKDF,
HMAC, X25519, ML-KEM-1024 and ML-DSA-65;
@noble/curves1.9.7 for secp256k1 and P-256 point arithmetic and RFC 6979 signing (a library by the same author as the curve code inside the ethers library that the producers use, in a different version), with every x-coordinate cross-checked against OpenSSL; and Keccak-256, EIP-55 and GF(2^8) arithmetic written in the harness. Version 1.0 of this text was written from the harness output. The harness reports 23 PASS: the 21 constructions of version 1.0 that have vectors, plus two meta-checks (a self-test of its own primitives, and a check that each built distribution is newer than its source). Five deliberate errors injected into its consumers (x-only KDF in place of the full point, SHA3-256 in place of Keccak-256, a wrong HKDF info string, a wrong salt label, a wrong naming label) each turned the matching construction red. - Text-first independent consumer. An independent review wrote a consumer from the text of
version 1.0 alone, with its own secp256k1 and P-256 arithmetic, its own Keccak-256 (checked against
the empty-string known answer), its own RFC 6979 and EIP-191 signing, and its own HKDF; only
AES-GCM, SHA-2/SHA-3, HMAC, ML-KEM and ML-DSA came from node:crypto. It opened every version 1.0
vector except those that lacked data (D.3, the D.4 links) and one truncated field, and it
established the facts added in version 1.1 by running the shipping code.
verify-v1.2.mjsis derived from that consumer. It adds its own FIPS 203 expansion of the ML-KEM decapsulation key (for D.4), and RSA signature verification and X25519 from node:crypto. - Final text-first check. A further independent review wrote a new consumer from the text of version 1.1 alone and opened every vector on its first run. Version 1.2 corrects the sentences that review found insufficient (D.1 key generation and number serialisation, the D.5 seat-format rule, the A.4 enclosing messages, and §4.3 on compressed keys).
B.3 Version 1.2 verification result. node verify-v1.2.mjs opens every vector in the file:
23 of 23 entries with vectors, 128 checks passed, 0 failed. Per entry: A.1 11, A.2 2, A.3 5,
A.4 15, A.5 6, A.6 5, B.1 4, B.2 8, C.1 3, C.2 5, C.3 2, D.1 12, D.2 2, D.3 3, D.4 10, D.5 7, D.6 5,
E.1 3, E.2 5, F.1 2, F.2 2, G.1 8, G.2 3. The A.4 count includes the direct-keys checks, among them a
negative check that the A.2 key derivation does not open a direct-keys 6.0 wrap. To check that the
consumer can fail, corrupted copies of the vectors file were run; each corruption was detected.
B.4 Statements established by reading source code. The statements marked [code-read] in the
text were not executed: where the A.1 member wrap's sender key comes from; the A.2 owner-self read
path; the assembly of the A.4 key-delivery, device-sync and direct-keys 4.0 messages (their wrapped-key
objects were executed; the direct-keys 6.0 message was executed end to end); the fields that the
shipping web consumer reads from a direct-keys message; the D.5 rules for a seat without alg, for an
unknown alg and for an unknown seatFormat; the D.6 rule that only local is accepted; which
vault-key rule (C.2, C.3) each component applies;
where each G.1 label is used and stored; and the G.2 helper output encoding. C.3 was produced by
evaluating its formula with the library function that the (non-exported) agent-runtime code calls.
B.5 To verify an entry.
- Take the test private keys or seeds from the entry's
vectorandinputs. - Recompute the reproducible values (public keys,
Z65/X32, derived keysK/KEK/WK, signatures from deterministic signers, ML-KEM public keys from seeds) and compare. - Decrypt each recorded ciphertext with the entry's Consume procedure and compare with the plaintext
given in
inputs. - Optionally produce a fresh value with your implementation and open it with your own Consume procedure, then compare field formats with the Wire format section.
B.6 Vector limitations.
rootz.hwkey.*: software stand-ins, not hardware-verified (Part G).- A.7 has no vector yet.
- The Epistery peer-cipher vectors include that library's own frozen vectors of 2026-09-30.
Appendix C — Revision history
| Version | Date | Change |
|---|---|---|
| 1.0 | 2026-10-16 | First version, written from executed known-answer tests. |
| 1.1 | 2026-10-16 | Revised after an independent assurance review. Scope defined (Section 1.1); entries A.6, A.7 and C.3 added; G.1 label defined as a stored parameter with both registered values; B.2 on-chain record form added; message fields for A.1, A.4, D.1 and D.5 specified (sender keys, seats, senderId, reWrapBy rule, mandatory signature check); D.1, D.2 and D.4 statuses corrected to exported-unused; full D.3 and D.4 vectors; every sub-vector's keys and plaintext stated; generic names, standards citations and known-limitation lists added; implementation detail not needed for interoperability removed. |
| 1.2 | 2026-10-16 | Revised after a final independent review. Scope stated by product (Section 1.1), with Epistery constructions other than A.3 and server-only code out of scope (Section 1.3); direct-keys 4.0 and 6.0 messages added to A.4, with vectors and the rule that the construction is selected by message type and version, not by the algorithm label; status definitions revised (Section 4.9), with D.5, D.6, A.3, C.3 and E.1 marked current (pre-release); the reserved-prefix entry removed; Appendix A lists packages and products instead of source paths; D.1 key generation (ML-DSA.KeyGen_internal) and number serialisation pinned; D.5 seat-format rule added; §1.4, §2, §4.3 and §4.6 corrected; F.2, D.2 and D.6 limitations restated as properties; G.1 vectors carry the public RSA test key only. |