# 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**: `C` and `T` are 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 without `0x`, 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 `ephemeralPub` field next to the encrypted key object.
- Member wrap: the encrypted key object carries **no** sender key. `Q_S` is 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):
```json
{ "ciphertext": "0x…32 bytes…", "iv": "0x…12 bytes…", "authTag": "0x…16 bytes…" }
```
*Device key entry* (ephemeral form):
```json
{ "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):
```json
{ "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):
```json
{ "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-package` `3.0` exists; 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 `MK` against 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 `K` per 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`, `deviceName` and 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.**
```json
{ "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.
```json
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_pair` is 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:
```json
{"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"`):
  `encryptedMasterKey` and `encryptedKVKey` are each the wrapped-key object **serialised as a JSON
  string**; `encryptedKVKey` may be `null`. `ephemeralPublicKey` is the sender's 65-byte uncompressed
  public key (`0x04…`). `encryptionMethod` is `"ephemeral-ecdh-v4"`. Both keys are wrapped under the
  **same** sender key, and therefore the same `K`, 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 of
  `ownedSecrets[]` and `teamShares[]` carries `encryptedMasterKey` and optional `encryptedKVKey` as
  wrapped-key **objects** (not strings). One top-level `ephemeralPublicKey` (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). `senderPublicKey` is the sender's 65-byte uncompressed
    public key, freshly generated for the message. The wrapped-key object is carried **flat**:
    `encryptedMasterKey` is its `encrypted` value (0x-hex `C ‖ T`) and `iv` is its `iv` value, 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 flat `senderPublicKey`, `encryptedMasterKey` and `iv` as
    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 boolean
    `hasKvKey`.
  - **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 the `algorithm` label.
  - A consumer obtains the master key from `senderPublicKey`, `encryptedMasterKey` and `iv`, and
    the vault key by the C.2 rule. The shipping web consumer reads only these three fields.
    **[code-read]**

**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, `K` is 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 `MK` and vault key `KVK`, 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); `Z65` is
  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.
```json
{ "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. `MK` and `KVK` are 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.**
```json
{ "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.**
```json
{ "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 `v` differently (for example as 0/1) or emits upper-case hex derives a
  different `K`, and the master key cannot be recovered with it.
- The message is constant per address: one `K` per owner for all secrets; random IVs only. No AAD;
  package fields and `extensions` are 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
    ```
- **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:*
```json
{ "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
```json
{ "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.**
```json
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 key `KVK` (32 bytes).
- **KVK wrap plaintext: hex text**, `utf8(hex0x(KVK))`, 66 bytes, produced from the key bytes and
  encrypted under `MK`.
- 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 **under `KVK`** in the C.1 CID form as
  `ipfsRef: { encrypted, iv, version: "5.0" }`, plus plain fields such as `filename`, `contentType`,
  `size`, `uploadedAt`.
- File content: the plaintext is the **base64 text** (RFC 4648) of the file bytes, i.e.
  `utf8(base64(file))`, encrypted under `KVK`.
- **Which vault key applies.** If the vault data carries a wrapped vault key, `KVK` is obtained by the
  KVK unwrap below. If it does not, the web dashboard and the agent package use the 32 master-key
  bytes themselves as `KVK`; 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)` with `seed = d(32) ‖ z(32)`. Public key 1568 bytes; ciphertext 1568
  bytes; shared secret 32 bytes.
- `recipientId`, `senderId`: strings. Both are KDF inputs.
- **Where `senderId` and the sender key are found.** The envelope carries neither `senderId` nor 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 own `recipientId` and 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`.
```json
{ "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`, `encapsulation` or
  `wrappedDek`. The recipient `epoch` field is not covered by the AAD or by the signature. A
  recipient's own `id` and `encapsulation` are bound to its wrap through the KDF and the GCM tag of
  `wrappedDek`; entries of other recipients are not authenticated to a recipient.
- `minVersion` is covered by the header, but see D.2: the agile dispatcher does not enforce it.
- `reWrapBy` is 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).
```json
{ "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"`.
```json
{"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`/`alg` to
  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 `n` gives 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)**, not `Z65` (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.
```json
{ "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):
```json
{ "seatFormat": 1,
  "memberPublicKey":  "0x04…65 bytes…",
  "granterPublicKey": "0x04…65 bytes…",
  "label": <string|null>,
  "grantedAt": "<ISO 8601 date-time>",
  "wrap": <wrap object> }
```
- `granterPublicKey` carries `Q_granter`, the classical-half public key a consumer needs.
- `wrap.alg` selects 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 `wrap` has **no** `alg` field is a classical seat (A.3). **[code-read]**
  - Any other `alg` value is rejected; there is no fallback. **[code-read]**
- A seat whose `seatFormat` is not `1` is rejected. **[code-read]**
- A seat with `wrap: null` (and `granterPublicKey: null`, `grantedAt: null`) is a pending seat with no
  key to open.
- `label` is 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 `KEK` comes 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.**
```json
{ "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 `recordName` nor any `clear` field is AAD.
- `clear` fields (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"))`, where `privText` is the agent's private key
  **exactly as configured** (in the vector, `"0x"` plus 64 lower-case hex digits). The same key
  without `0x`, or in upper case, gives a different `K` (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 `A` and the enclave public key can compute `S`; the
  secrecy of `priv` then rests on the entropy alone. The application states that the private key of
  `A` was discarded; `A` is 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.

1. **Producer.** For every vector, the shipping Rootz implementation (its built distribution, or the
   shipped module) produced the values from test keys. Vectors marked `reverse` were produced by an
   independent implementation and opened by the shipping implementation.
2. **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/curves` 1.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.
3. **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.mjs` is
   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.
4. **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.**
1. Take the test private keys or seeds from the entry's `vector` and `inputs`.
2. Recompute the reproducible values (public keys, `Z65`/`X32`, derived keys `K`/`KEK`/`WK`,
   signatures from deterministic signers, ML-KEM public keys from seeds) and compare.
3. Decrypt each recorded ciphertext with the entry's Consume procedure and compare with the plaintext
   given in `inputs`.
4. 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. |
