Untilum
← Back to Untilum Protocol

Capsule Format v1 — Specification

Status: Draft (normative for this repository; the wire format of the open Untilum Protocol — see tsp-v1.md) Date: 2026-07-03 (v1.1: encrypted metadata added the same day) Reference implementation: packages/core (TypeScript). A conforming independent implementation MUST interoperate with the reference: capsules sealed by one MUST open in the other.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals.


1. Purpose and model

A capsule is a self-contained JSON document that encrypts a byte payload such that decryption requires both, independently:

  1. The time factor — the BLS signature of drand quicknet round R, which does not exist anywhere in the universe before round R is emitted; and
  2. The password factor — a user-held password, never stored anywhere.

Neither factor alone suffices. The platform that created a capsule is not required for opening it: a capsule plus a beacon source plus the password is sufficient. Any implementation feature that makes opening depend on a specific server or domain is non-conforming.

1.1 Trust assumptions (informative)


2. Cryptographic scheme (normative)

2.1 Envelope

tlsecret   = CSPRNG(32 bytes)                                  # time factor carrier
salt       = CSPRNG(16 bytes)
pwKey      = KDF(password, salt)              -> 32 bytes      # §2.2
fileKey    = HKDF-SHA256(IKM = tlsecret || pwKey,
                         salt = salt,
                         info = "capsule-v1", L = 32)
metaKey    = HKDF-SHA256(same IKM, same salt,
                         info = "capsule-v1-meta", L = 32)     # §2.5
iv         = CSPRNG(12 bytes)
ciphertext = AES-256-GCM(key = fileKey, iv = iv, plaintext)    # 16-byte tag APPENDED
metaEnc    = { iv: CSPRNG(12), ct: AES-256-GCM(metaKey, iv, UTF-8 JSON of meta) }
tl         = tlock_encrypt(round = R, payload = tlsecret)      # §2.4

Opening reverses the derivation. A wrong password manifests as an AES-GCM tag failure. A missing, future, or forged round manifests as a tlock/BLS verification failure.

2.2 Password KDF

The capsule self-describes its KDF in alg.kdf / alg.kdfParams. Two KDFs are defined:

alg.kdf Algorithm Params (alg.kdfParams) Status
argon2id Argon2id (RFC 9106) { m, t, p, dkLen }, m in KiB Current. All new capsules MUST use it.
pbkdf2 PBKDF2-HMAC-SHA256 { c, dkLen } Legacy. Openers MUST support; sealers MUST NOT emit.

Production Argon2id parameters: m = 65536 (64 MiB), t = 3, p = 1, dkLen = 32. Legacy PBKDF2 parameters: c = 300000, dkLen = 32.

Anti-downgrade floors. An opener MUST reject (before running the KDF) any capsule whose parameters fall below the minima ever emitted by a conforming sealer:

Rationale: the ciphertext is public forever (permanent storage); after round R the password is the only remaining factor. A crafted capsule with weakened parameters must not be able to trick an opener into deriving a cheap-to-brute-force key.

2.3 Factor combination

HKDF-SHA256 (RFC 5869) with the capsule’s 16-byte salt as HKDF salt and info = "capsule-v1", output 32 bytes. The IKM is tlsecret || pwKey (64 bytes).

2.4 Timelock

tl is the output of tlock (age v1 / ChaCha20-Poly1305 payload wrapping, IBE on BLS12-381 per the tlock paper, IACR 2023/189) encrypting the 32-byte tlsecret to the identity of round R on drand quicknet only:

Constant Value
chain hash 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971
group public key (G2, 96 B) 83cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d1064510d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a
scheme bls-unchained-g1-rfc9380 (signatures on G1)
period 3 s
genesis 1692803367 (unix s)

Round identity: SHA-256(round as 8-byte big-endian). Round from time: R = floor((T_unix − genesis) / period) + 1, clamped to ≥ 1.

The deprecated fastnet chain (dbd506d6…) MUST be rejected everywhere.

Beacon verification. Before use, every round signature — regardless of source (drand HTTP API, relay, mirror, manual paste) — MUST be BLS-verified against the embedded quicknet group public key, and its round number MUST equal the capsule’s round. Implementations MUST NOT expose an option to skip verification.

2.5 Metadata encryption (v1.1)

Metadata ({ name, mime, createdAt } as UTF-8 JSON) is encrypted with AES-256-GCM under metaKey, a key derived from the same IKM and salt as fileKey but with the distinct HKDF info string capsule-v1-meta. The domain separation guarantees the file ciphertext and the metadata ciphertext can never be substituted for one another. Metadata therefore requires BOTH factors to read — exactly like the payload.

Sealers MUST emit metaEnc and MUST NOT emit plaintext meta. Openers MUST support legacy capsules that carry plaintext meta instead (pre-v1.1).

The metadata JSON is application-extensible and never validated structurally. The standard field names (all OPTIONAL, all advisory — shared by every reference application so that any opener can render them; v2 capsules use the same names):

Field Meaning
name original file name, with extension (restored on open)
mime payload MIME type
title human-readable capsule title
createdAt sealing instant, ISO-8601 UTC
location where the capsule was created — free text, as the creator wants it remembered
device device / application the capsule was created on
owner display name of the capsule’s creator/owner

Sensitive values belong exactly here: metaEnc requires both factors to read. None of these fields may ever appear in the plaintext capsule document (security invariant: no sensitive metadata in the open payload).


3. Capsule document format (normative)

A capsule is a single JSON object, UTF-8 encoded. Unknown top-level fields MUST be rejected by strict validators (the reference backend does) and MUST NOT be emitted.

{
  "v": 1,                       // format version, integer, MUST be 1
  "alg": {
    "sym":      "AES-256-GCM",  // exactly this string
    "kdf":      "argon2id",     // or "pbkdf2" (legacy) — §2.2
    "kdfParams": { "m": 65536, "t": 3, "p": 1, "dkLen": 32 },
    "combine":  "HKDF-SHA256",
    "timelock": "tlock-quicknet"
  },
  "round": 123456789,           // drand quicknet round R, integer ≥ 1
  "chainHash": "52db9ba7…e971", // MUST equal the quicknet chain hash
  "salt": "<32 hex chars>",     // 16 bytes, lowercase hex
  "iv":   "<24 hex chars>",     // 12 bytes, lowercase hex
  "ciphertext": "<hex>",        // AES-GCM body || 16-byte tag, lowercase hex
                                //   OR an "ar://<txid>" reference (reserved, §6)
  "tl": "-----BEGIN AGE ENCRYPTED FILE-----…", // age-armored tlock blob (ASCII)
  "metaEnc": {                  // v1.1 — encrypted metadata (§2.5)
    "iv": "<24 hex chars>",     // 12 bytes
    "ct": "<hex>"               // AES-GCM(metaKey, iv, JSON {name,mime,createdAt}) || tag
  }
  // LEGACY (pre-v1.1) capsules carry instead:
  // "meta": { "name": "photo.jpg", "mime": "image/jpeg", "createdAt": "…" }
  // — plaintext, publicly readable. Exactly ONE of metaEnc / meta MUST be present.
}

Validation an opener MUST perform before any cryptography:

  1. v === 1; all fields above present.
  2. chainHash equals the quicknet hash (constant-string comparison).
  3. round is a positive integer.
  4. alg.sym === "AES-256-GCM", alg.timelock === "tlock-quicknet".
  5. alg.kdf ∈ {argon2id, pbkdf2} and kdfParams meet the floors of §2.2.
  6. salt, iv, ciphertext, tl are non-empty strings of the stated shapes.
  7. Exactly one of metaEnc (well-formed iv/ct hex) or legacy meta (string name/mime/createdAt) is present.

Hex is lowercase on emission; parsers SHOULD accept uppercase.


4. Procedures

4.1 Seal

  1. Determine R from the user’s open date (§2.4). Normalize dates to UTC; show the user the exact UTC instant R maps back to.
  2. Generate tlsecret (32 B), salt (16 B) from a CSPRNG.
  3. pwKey = Argon2id(password, salt, m=65536, t=3, p=1, dkLen=32).
  4. fileKey = HKDF-SHA256(tlsecret || pwKey, salt, "capsule-v1", 32).
  5. AES-256-GCM-encrypt the payload with a fresh 12-byte IV.
  6. tl = tlock_encrypt(R, tlsecret) using the pinned chain info (no network needed).
  7. Emit the JSON document of §3. Zeroize tlsecret, pwKey, fileKey.

Sealing MUST be possible fully offline (the quicknet public key is pinned).

4.2 Open

  1. Validate the document (§3).
  2. Obtain the beacon for round from any source; BLS-verify it (§2.4).
  3. tlsecret = tlock_decrypt(tl, beacon).
  4. Derive pwKey per the capsule’s alg (after the floor check), then fileKey, then AES-256-GCM-decrypt. Tag failure ⇒ report “wrong password or corrupted capsule” — the two are cryptographically indistinguishable.
  5. Zeroize secrets.

An opener SHOULD attempt beacon sources in order: live drand API → relay → mirror → manual paste, and MUST verify each identically.


5. Known limitations of v1 (normative warnings)

  1. Legacy capsules leak metadata. Capsules sealed before v1.1 carry plaintext meta.name/meta.mime on public permanent storage forever — this cannot be repaired retroactively. Since v1.1 metadata is encrypted (§2.5); UIs MUST still NOT put sensitive titles into any plaintext field (e.g. a UI display name kept outside the capsule).
  2. Inline hex ciphertext doubles the stored size (and the user’s storage cost) and requires whole-file memory. v1 is therefore limited to small payloads (2 MB in the reference web app).
  3. Single beacon chain. If quicknet ceases before round R is emitted, the capsule becomes unopenable until a cryptographically relevant quantum computer exists. There is no in-format mitigation in v1.
  4. No integrity/creation proof. The format carries no signature of the creator and no proof of creation time other than the storage layer’s own timestamp.

6. Sketch: v2 directions (informative, not yet normative)


7. Test vectors (normative)

A conforming implementation MUST reproduce these exactly. (Recomputed in CI by packages/core/src/vectors.test.ts; embedded in the offline decryptor’s self-test.)

7.1 PBKDF2 (legacy KDF)

password  = "correct horse battery staple"   (UTF-8)
salt      = 000102030405060708090a0b0c0d0e0f
iterations= 300000, dkLen = 32
output    = 48a269a707488c7db1ce99c1e4ba8d3204cd7a8020e6928a193c47e6e5d0dbaa

7.2 Argon2id (current KDF, production params)

password  = "correct horse battery staple"   (UTF-8)
salt      = 000102030405060708090a0b0c0d0e0f
m = 65536 KiB, t = 3, p = 1, dkLen = 32
output    = 0d1a3c6523c8f06e4e0af9c515aa5b5448cfebd6838f2d52c3d8b6ef8ddc3c2e

7.3 HKDF-SHA256 (factor combination)

IKM   = aa×32 || bb×32   (64 bytes)
salt  = 000102030405060708090a0b0c0d0e0f
info  = "capsule-v1", L = 32
output= 74881992e7f20fa3c8a3c561e332dc59bc70c7855b36192b9c1b2106103dd9e4

7.4 AES-256-GCM

key = 2b7e151628aed2a6abf7158809cf4f3c2b7e151628aed2a6abf7158809cf4f3c
iv  = cafebabefacedbaddecaf888
plaintext  = 54696d652043617073756c65204b4154207631   ("Time Capsule KAT v1")
ciphertext||tag =
  86bb3ceae723f01007c794e8a631b520051563e3529503cbd650500a195c906522d45f

7.5 Round computation

date  = 2030-01-01T00:00:00Z
round = 66884212

The timelock layer itself is non-deterministic (random sigma, ephemeral age key), so tlock has no byte-exact KAT; interop is tested by round-tripping capsules between implementations instead.


8. Security invariants for implementations (summary)

  1. All encryption client-side; no server ever receives plaintext, password, or keys.
  2. Opening must work with zero dependence on the creating platform.
  3. Two factors always; no password recovery; no single-factor mode.
  4. Audited primitives only (tlock, Web Crypto, noble, hash-wasm); no custom crypto.
  5. Offline decryptors inline every dependency; no CDN/network code loading.
  6. drand quicknet only; fastnet rejected.
  7. Every beacon BLS-verified before use; verification is not optional.
  8. KDF parameter floors enforced at parse time (anti-downgrade, §2.2).

Revision History

Date Change
2026-07-03 Initial version; v1.1 (encrypted metadata) added the same day.
2026-07-17 Publication pass for the series Last Call: BCP 14 boilerplate. Specification remains Draft; the v1 wire format itself is stable and covered by published test vectors.