cipher: std/cipher with ChaCha20-Poly1305 (RFC 8439) #264

Closed
opened 2026-10-09 11:06:37 +00:00 by orion · 9 comments
Collaborator

Goal

std/cipher: ChaCha20-Poly1305 authenticated encryption (RFC 8439) in TorbScript, with the RFC's test vectors. The registry needs it to store TOTP seeds encrypted (#227); std has no authenticated cipher today (mbedTLS's ChaCha20-Poly1305 is used inside TLS only and is not reachable from TorbScript).

Why a std slice and not a private registry module

Decided in refine of #227: a cipher is not registry logic. It has published vectors (RFC 8439 sections 2.1 to 2.8 and appendix A), so it can be tested on its own, and tools/registry/src/rsa.trb and ed25519 show the cost of the other path: a primitive written privately has to be moved later (FIRST-PARTY.md section 8, row 6; #110). It also takes the cipher out of #227, which is already one PR of TOTP, recovery codes, rate limit, account page and tests.

Pinned records

  • RFC 8439 (ChaCha20 section 2.3 and 2.4, Poly1305 section 2.5, the AEAD construction section 2.8, test vectors 2.1.1, 2.2.1, 2.3.2, 2.4.2, 2.5.2, 2.6.2, 2.8.2 and appendix A.1 to A.5), RFC 7539 as the earlier text.
  • RELEASE.md 7.6 Security; parent #16, consumer #227.
  • Model on std/signature (package layout, SignatureError, test and docs layout) and std/digest (Bytes, constantTimeEqual).

Acceptance criteria

  • New package std/cipher (directory std/cipher, project.trb with name = "std/cipher", src/lib.trb, tests/); the workspace member glob std/* already covers it.
  • ChaCha20Poly1305, a value keyed with exactly 32 bytes (ChaCha20Poly1305.tryFrom(key)-style constructor in the style of std/signature's key types; a wrong length is a CipherError.WrongLength), with methods seal(nonce, plaintext, associated) (answers ciphertext followed by the 16-byte tag) and open(nonce, sealed, associated) (answers the plaintext, or CipherError.Forged for any failure of the tag, with no detail of where it differs); the nonce is exactly 12 bytes. Methods, not free functions (#146).
  • The tag is compared in constant time (std/digest's constantTimeEqual); open releases no plaintext before the tag is checked.
  • Only the AEAD is public; the ChaCha20 block function and Poly1305 are private to the package (tested through the AEAD and, in the package's own tests, directly).
  • Tests: every RFC 8439 vector above (block function, keystream, encryption, Poly1305 key generation, MAC, the AEAD), plus: every vector once as it works and once as it is refused (changed ciphertext, changed tag, changed associated data, changed nonce, wrong key, truncated input below 16 bytes, empty plaintext sealed and opened, associated data empty and non-empty); torb test std/cipher/tests green natively and in the VM.
  • docs/standard-library/cipher.md and the index of docs/standard-library/ document the new types and say: a nonce must never repeat under one key (random 96-bit nonces are safe for up to about 2^32 messages per key), what is and is not constant time (the tag comparison is; the arithmetic is plain Int and promises nothing more), and the measured speed for a 32-byte message and for 1 MiB in the VM and natively; skills/ regenerated (docs skill).

Seed impact

None: a new std package in plain TorbScript over Int/UInt32 arithmetic and std/digest's constantTimeEqual. No syntax change, no native, no runtime change, no driver flag, and the compiler looks up none of the new names by string. Additive only; no seed refresh, so no two-commit rule.

Size and dependencies

One PR, one feat(std/cipher) commit (code, tests, docs). No dependency. Blocks #227. Security-relevant: architect review of the diff.

## Goal `std/cipher`: ChaCha20-Poly1305 authenticated encryption (RFC 8439) in TorbScript, with the RFC's test vectors. The registry needs it to store TOTP seeds encrypted (#227); `std` has no authenticated cipher today (mbedTLS's ChaCha20-Poly1305 is used inside TLS only and is not reachable from TorbScript). ## Why a std slice and not a private registry module Decided in refine of #227: a cipher is not registry logic. It has published vectors (RFC 8439 sections 2.1 to 2.8 and appendix A), so it can be tested on its own, and `tools/registry/src/rsa.trb` and `ed25519` show the cost of the other path: a primitive written privately has to be moved later (FIRST-PARTY.md section 8, row 6; #110). It also takes the cipher out of #227, which is already one PR of TOTP, recovery codes, rate limit, account page and tests. ## Pinned records - RFC 8439 (ChaCha20 section 2.3 and 2.4, Poly1305 section 2.5, the AEAD construction section 2.8, test vectors 2.1.1, 2.2.1, 2.3.2, 2.4.2, 2.5.2, 2.6.2, 2.8.2 and appendix A.1 to A.5), RFC 7539 as the earlier text. - [RELEASE.md 7.6 Security](https://git.torb.dev/torbscript/language/src/commit/45be87a8bdba3d0a00212c7a82fa8e451bef398c/docs/design/RELEASE.md#L1029); parent #16, consumer #227. - Model on `std/signature` (package layout, `SignatureError`, test and docs layout) and `std/digest` (`Bytes`, `constantTimeEqual`). ## Acceptance criteria - [ ] New package `std/cipher` (directory `std/cipher`, `project.trb` with `name = "std/cipher"`, `src/lib.trb`, `tests/`); the workspace member glob `std/*` already covers it. - [ ] `ChaCha20Poly1305`, a value keyed with exactly 32 bytes (`ChaCha20Poly1305.tryFrom(key)`-style constructor in the style of `std/signature`'s key types; a wrong length is a `CipherError.WrongLength`), with methods `seal(nonce, plaintext, associated)` (answers ciphertext followed by the 16-byte tag) and `open(nonce, sealed, associated)` (answers the plaintext, or `CipherError.Forged` for any failure of the tag, with no detail of where it differs); the nonce is exactly 12 bytes. Methods, not free functions (#146). - [ ] The tag is compared in constant time (`std/digest`'s `constantTimeEqual`); `open` releases no plaintext before the tag is checked. - [ ] Only the AEAD is public; the ChaCha20 block function and Poly1305 are private to the package (tested through the AEAD and, in the package's own tests, directly). - [ ] Tests: every RFC 8439 vector above (block function, keystream, encryption, Poly1305 key generation, MAC, the AEAD), plus: every vector once as it works and once as it is refused (changed ciphertext, changed tag, changed associated data, changed nonce, wrong key, truncated input below 16 bytes, empty plaintext sealed and opened, associated data empty and non-empty); `torb test std/cipher/tests` green natively and in the VM. - [ ] `docs/standard-library/cipher.md` and the index of `docs/standard-library/` document the new types and say: a nonce must never repeat under one key (random 96-bit nonces are safe for up to about 2^32 messages per key), what is and is not constant time (the tag comparison is; the arithmetic is plain `Int` and promises nothing more), and the measured speed for a 32-byte message and for 1 MiB in the VM and natively; `skills/` regenerated (`docs skill`). ## Seed impact None: a new std package in plain TorbScript over `Int`/`UInt32` arithmetic and `std/digest`'s `constantTimeEqual`. No syntax change, no native, no runtime change, no driver flag, and the compiler looks up none of the new names by string. Additive only; no seed refresh, so no two-commit rule. ## Size and dependencies One PR, one `feat(std/cipher)` commit (code, tests, docs). No dependency. Blocks #227. Security-relevant: architect review of the diff.
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop), split out of #227 by its refine (cipher as its own std slice), not a duplicate, no seed impact, no dependency, blocks #227. Body already meets the definition of ready; implementation is security-relevant (sonnet/medium for std, architect review of the diff). next: refine (confirm and label ready)

[loop:torbscript] intake: in scope (author orion, label loop), split out of #227 by its refine (cipher as its own std slice), not a duplicate, no seed impact, no dependency, blocks #227. Body already meets the definition of ready; implementation is security-relevant (sonnet/medium for std, architect review of the diff). next: refine (confirm and label ready)
Author
Collaborator

[loop:torbscript] refine: the body written by the refine of #227 meets the definition of ready (goal, testable criteria with RFC 8439 vectors, pinned records, no seed impact, one PR). Set ready. next: ready (implement)

[loop:torbscript] refine: the body written by the refine of #227 meets the definition of ready (goal, testable criteria with RFC 8439 vectors, pinned records, no seed impact, one PR). Set `ready`. next: ready (implement)
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-09T17:10Z

[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-09T17:10Z
Author
Collaborator

[loop:torbscript] release implement: the implementation worker ended after about 23 minutes before committing, no branch pushed and no PR opened, nothing to salvage. next: implement (re-dispatch on the next tick)

[loop:torbscript] release implement: the implementation worker ended after about 23 minutes before committing, no branch pushed and no PR opened, nothing to salvage. next: implement (re-dispatch on the next tick)
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-09T17:40Z

[loop:torbscript] claim implement by session:loop-torbscript-20261009 until 2026-10-09T17:40Z
Author
Collaborator

[loop:torbscript] implement: progress - PR #266 is open, argus requested. Fixed the declaration block (Encode) that the docs gate refused; the previous tier A also failed in a language-server test that fails on main too (90e233aa), so I rebased onto current main (2a1d8ae3) and CI runs again. Still holding the claim until CI is read.

[loop:torbscript] implement: progress - PR https://git.torb.dev/torbscript/language/pulls/266 is open, argus requested. Fixed the declaration block (Encode) that the docs gate refused; the previous tier A also failed in a language-server test that fails on main too (90e233aa), so I rebased onto current main (2a1d8ae3) and CI runs again. Still holding the claim until CI is read.
argus closed this issue 2026-10-09 13:53:05 +00:00
Author
Collaborator

[loop:torbscript] release implement: PR #266 approved and merged by argus (2026-10-09T13:53Z, merge commit 2a1d8ae3); this issue closed by the merge. No release or deploy step in the PR. Consumer #227 can now use std/cipher. next: none for #264

[loop:torbscript] release implement: PR https://git.torb.dev/torbscript/language/pulls/266 approved and merged by argus (2026-10-09T13:53Z, merge commit 2a1d8ae3); this issue closed by the merge. No release or deploy step in the PR. Consumer #227 can now use std/cipher. next: none for #264
Author
Collaborator

[loop:torbscript] implement: PR #266, CI green (pull_request run on 2a1d8ae3: gates, tier A, bootstrap, same-C, playground, seed pack), next: review (waiting on the merge gate argus)

Token usage: not reported by the worker session (two sessions; the first ended without a push); an architect review of the diff ran before the PR and its blocking finding (derived Encode leaked the key) is fixed.

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/266, CI green (pull_request run on 2a1d8ae3: gates, tier A, bootstrap, same-C, playground, seed pack), next: review (waiting on the merge gate argus) Token usage: not reported by the worker session (two sessions; the first ended without a push); an architect review of the diff ran before the PR and its blocking finding (derived Encode leaked the key) is fixed.
Author
Collaborator

[loop:torbscript] close: PR #266 approved and merged by argus as 2a1d8ae3; main CI green (tier B red on 2a1d8ae3 was the unrelated child-close race #270, fixed by #271, main green on 0a129c2e). Claim released.

[loop:torbscript] close: PR https://git.torb.dev/torbscript/language/pulls/266 approved and merged by argus as 2a1d8ae3; main CI green (tier B red on 2a1d8ae3 was the unrelated child-close race #270, fixed by #271, main green on 0a129c2e). Claim released.
Sign in to join this conversation.
No labels
loop
ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
torbscript/language#264
No description provided.