Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
package main
import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
Format 3, step 6b: the nine fixtures of format 3
The fixtures of spec 67 for format 3, each with its record, its BODY,
its inspect output and, for time_and_key, its .dkk:
- format3_single, format3_tree (five files in three folders, one over
two STREAM chunks, one without mtime, a comment and a declared
author), format3_comment_only (no files; a TAB in the comment),
format3_bloque256 and format3_time_and_key_portable, written with
EncryptFiles;
- format3_area_1024, format3_security_v2 (verdict X),
format3_signature_unsupported (an author-signature of alg 1 with a
random key of 32 bytes and a random signature of 64: F1) and
format3_seal_unsupported (that and a seal of seal_type 1: F1 and S1),
which only a generator of test vectors writes (62.1 rule 13), built
with testkit.Build.
The record of a format 3 fixture adds the area, SECURITY_CBOR,
HEAD_CBOR, the salt, the comment, the declared author, the head
extensions, the offset of CONTENT in BODY, each file with its layout,
SHA-256 and mtime, and the verdicts with their lines; its plaintext
file is BODY. The generator writes, then recovers every value by
opening layer by layer for the three formats alike, and refreshes the
records of format 3 through a Sink.
Tests: the conformance test checks BODY, the head, security and every
file, and opens through a MemorySink; the .dkk tests take the .dkk of
formats 2 and 3 too (spec 68); the CLI decrypts five of the fixtures
into folders; the control fuzz target decodes with the three schema
versions. The differential corpus gains two bases, format3_single and
format3_time_and_key_portable, one per policy: 5110 cases, the earlier
ones unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
"reflect"
Version constants: the specification and the module
- datekeys.SpecVersion ("0.8.2") names the specification the module
implements. A test ties it to the spec file, its title and
spec/README.md, and TestCatalogueMatchesSpec and the vector files use
it (testkit.SpecVersion now aliases it), so the vectors regenerate
unchanged.
- datekeys.Version() is the version of the module as the go command
recorded it. That is a tag, or for a binary built in a checkout the
pseudo-version of its commit (for example
v0.0.0-20260928105528-9ac9cd952f04), or (devel) when it is unknown, as
in tests or under a replace directive to a directory. It works as the
main module and as a dependency, whatever the module path, which it
reads from the root package.
- `datekeys version` (also -version and --version) prints both and the
Go toolchain.
- README.md and README.es.md explain the three versions (format,
specification, module) and what the code on main covers.
traceability §70 and CHANGELOG follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
"runtime"
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
"strings"
"testing"
"time"
"filippo.io/age"
datekeys "g.activething.com/go/DateKeys"
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/inspectview"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/profile"
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
)
const fixtures = "../../testdata/fixtures"
// relay serves the known Quicknet releases like a drand HTTP relay.
func relay ( t * testing . T ) string {
t . Helper ( )
p := profile . Quicknet ( )
s := httptest . NewServer ( http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) {
for _ , round := range testkit . Rounds {
if r . URL . Path == fmt . Sprintf ( "/v2/chains/%s/rounds/%d" , p . ChainHashHex ( ) , round ) {
fmt . Fprintf ( w , ` { "round":%d,"signature":"%s"} ` , round , hex . EncodeToString ( testkit . Release ( round ) . Signature ) )
return
}
}
http . NotFound ( w , r )
} ) )
t . Cleanup ( s . Close )
return s . URL
}
func cli ( t * testing . T , now time . Time , args ... string ) ( string , string , error ) {
t . Helper ( )
var out , errOut bytes . Buffer
err := run ( args , & out , & errOut , func ( ) time . Time { return now } )
return out . String ( ) , errOut . String ( ) , err
}
var later = time . Date ( 2026 , 9 , 25 , 12 , 0 , 0 , 0 , time . UTC )
Review fixes: author keys, the writer, the CLI, extensions and the locator
Fixes of the review of the session of 1 and 2 October that the text of
spec v0.11 already asks for:
- authorkey: String and GoString hide the secret key, which only Secret
returns; ParsePublic refuses a key that is not a point of the curve
(ed25519strict.OnCurve, checked against the square root of testkit).
- capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never
a capsule without the signature or the seal that was asked for. A panic
while evaluating the signature or the seal fails only that part, F1 or
S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can
refuse to publish the files.
- extension.CheckWrite, the rule of encoders of spec 72: the writers of
capsules and .dkk files refuse datekeys.note and datekeys.capsule outside
the arrays where they are registered, or with invalid data.
- CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE
before it signs (rule 20); decrypt -expect-author compares the key of an
F4 and writes nothing unless it matches; decrypt notifies a public note
that it does not show; the lines of the verdicts break at the last space
that fits, each row after the first behind a mark, so that the terminal
never breaks them; L is the payload, not the content.
- locator: a reader rejects an address that breaks 44.1 and keeps the
others; addresses refuse the special-purpose blocks of IANA, IPv6 outside
2000::/3, localhost and local names, characters outside RFC 3986, dot
segments, and a CID that does not decode to version 1 and a multihash;
ParseInfo checks that the locator is an age file with one tlock stanza
for the round of its DateKey; Info.Extension reads what it writes; its
errors carry no normative code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
// joined undoes the rows of writeVerdicts: each row after the first of a line
// goes back after the space that its break dropped. No line of the tests
// breaks inside a word at 80 columns.
func joined ( s string ) string { return strings . ReplaceAll ( s , "\n" + contMark , " " ) }
// The lines of the reader break at the last space that fits, behind the mark
// of a continuation; a word longer than a row breaks inside; and the
// indentation of a line is not a place to break it.
func TestRows ( t * testing . T ) {
for _ , c := range [ ] struct {
line string
first , rest int
want [ ] string
} {
{ "abc def ghi" , 20 , 20 , [ ] string { "abc def ghi" } } ,
{ "abc def ghi" , 7 , 7 , [ ] string { "abc def" , "ghi" } } ,
{ "abc def ghi" , 6 , 6 , [ ] string { "abc" , "def" , "ghi" } } ,
{ "abcdefghij k" , 4 , 3 , [ ] string { "abcd" , "efg" , "hij" , "k" } } ,
{ " abcdefgh" , 5 , 5 , [ ] string { " abc" , "defgh" } } ,
{ "ñañañaña" , 4 , 4 , [ ] string { "ña" , "ña" , "ña" , "ña" } } ,
} {
if got := rows ( c . line , c . first , c . rest ) ; ! reflect . DeepEqual ( got , c . want ) {
t . Errorf ( "rows(%q, %d, %d) = %q, want %q" , c . line , c . first , c . rest , got , c . want )
}
}
var b strings . Builder
writeVerdicts ( & b , [ ] string { "Firmado con la clave que guardaste como Mamá." } , 20 )
if got := b . String ( ) ; got != "Firmado con la\n" + contMark + "clave que\n" + contMark + "guardaste\n" + contMark + "como Mamá.\n" {
t . Errorf ( "writeVerdicts at 20 columns:\n%s" , got )
}
}
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
func TestOutputNotPublishedOnFailureOrOverwrite ( t * testing . T ) {
dir := t . TempDir ( )
out := filepath . Join ( dir , "output" )
err := writeAtomic ( out , func ( w io . Writer ) error {
_ , _ = w . Write ( [ ] byte ( "partial plaintext" ) )
return errors . New ( "invalid authentication tag" )
} )
if err == nil {
t . Fatal ( "expected failure" )
}
if _ , err := os . Stat ( out ) ; ! errors . Is ( err , os . ErrNotExist ) {
t . Fatal ( "published partial output" )
}
if err := os . WriteFile ( out , [ ] byte ( "keep me" ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
if err := writeAtomic ( out , func ( io . Writer ) error { t . Fatal ( "should not run" ) ; return nil } ) ; err == nil {
t . Fatal ( "overwrote output" )
}
if b , _ := os . ReadFile ( out ) ; string ( b ) != "keep me" {
t . Fatal ( "output changed" )
}
if files , _ := filepath . Glob ( filepath . Join ( dir , ".datekeys-*" ) ) ; len ( files ) != 0 {
t . Fatal ( "temporary file left behind" )
}
if err := copyExclusive ( out , out ) ; err == nil {
t . Fatal ( "exclusive copy replaced a file" )
}
}
func TestDecryptFixtures ( t * testing . T ) {
url := relay ( t )
for _ , tc := range [ ] struct { name , dkk string } {
{ "time_only" , "" } ,
{ "time_only_extensions" , "" } ,
{ "empty_payload" , "" } ,
{ "time_and_key_portable" , "time_and_key_portable.dkk" } ,
Implement capsule format 2 of spec v0.9
The reference moves to the DateKeys Protocol Specification v0.9, approved
by its author on 29 September 2026. Encrypt writes capsule format 2 only;
Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the
verdict v0.8.2 gave it.
Format 2 (spec §22, §29.1, §31, §39):
- VERSION in the PRELUDE is the capsule format, capsule.Format; any other
value is ERR_UNSUPPORTED_VERSION at step 2.
- CONTROL_CBOR has the schema version of its format. Version 2 adds key 6,
payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and
key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without
extensions, whatever L.
- The payload is the content padded with zeros to P = rule(L). Step 17
checks the length and the zeros, and Open writes only the first L bytes.
- INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials,
and a dummy in each slot left, in a uniformly random order.
Writer rules (spec §62.1): EncryptOptions.Length is required and the
source must deliver exactly that many bytes; recipients that are not
canonical or of low order are rejected (agewrap.CheckX25519Recipient);
self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE.
The CLI measures its input, takes -padding and reports the format.
Test data: seven format 2 fixtures, padding vectors checked against
math/big, format 2 CBOR vectors, and the mutation corpus in both formats
with the 22 cases of the third list of spec §64, built without randomness
by sealing the fixtures again with their known keys and nonces. The
format 1 fixtures are kept byte for byte and never regenerated; the
differential corpus keeps its 1825 cases and adds a block per format 2
fixture. The spec copy loses its "to be implemented" markers, and the
READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
{ "format2_time_only" , "" } ,
{ "format2_time_only_bloque256" , "" } ,
{ "format2_empty_payload" , "" } ,
{ "format2_time_only_extensions" , "" } ,
{ "format2_time_and_key_portable" , "format2_time_and_key_portable.dkk" } ,
{ "format2_time_and_key_recipients" , "format2_time_and_key_recipients.dkk" } ,
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
} {
t . Run ( tc . name , func ( t * testing . T ) {
out := filepath . Join ( t . TempDir ( ) , "plain" )
args := [ ] string { "decrypt" , "-in" , filepath . Join ( fixtures , tc . name + ".dkc" ) , "-out" , out , "-relay" , url }
if tc . dkk != "" {
args = append ( args , "-dkk" , filepath . Join ( fixtures , tc . dkk ) )
}
Implement capsule format 2 of spec v0.9
The reference moves to the DateKeys Protocol Specification v0.9, approved
by its author on 29 September 2026. Encrypt writes capsule format 2 only;
Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the
verdict v0.8.2 gave it.
Format 2 (spec §22, §29.1, §31, §39):
- VERSION in the PRELUDE is the capsule format, capsule.Format; any other
value is ERR_UNSUPPORTED_VERSION at step 2.
- CONTROL_CBOR has the schema version of its format. Version 2 adds key 6,
payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and
key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without
extensions, whatever L.
- The payload is the content padded with zeros to P = rule(L). Step 17
checks the length and the zeros, and Open writes only the first L bytes.
- INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials,
and a dummy in each slot left, in a uniformly random order.
Writer rules (spec §62.1): EncryptOptions.Length is required and the
source must deliver exactly that many bytes; recipients that are not
canonical or of low order are rejected (agewrap.CheckX25519Recipient);
self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE.
The CLI measures its input, takes -padding and reports the format.
Test data: seven format 2 fixtures, padding vectors checked against
math/big, format 2 CBOR vectors, and the mutation corpus in both formats
with the 22 cases of the third list of spec §64, built without randomness
by sealing the fixtures again with their known keys and nonces. The
format 1 fixtures are kept byte for byte and never regenerated; the
differential corpus keeps its 1825 cases and adds a block per format 2
fixture. The spec copy loses its "to be implemented" markers, and the
READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
_ , stderr , err := cli ( t , later , args ... )
if err != nil {
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
t . Fatalf ( "%v\n%s" , err , stderr )
}
Implement capsule format 2 of spec v0.9
The reference moves to the DateKeys Protocol Specification v0.9, approved
by its author on 29 September 2026. Encrypt writes capsule format 2 only;
Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the
verdict v0.8.2 gave it.
Format 2 (spec §22, §29.1, §31, §39):
- VERSION in the PRELUDE is the capsule format, capsule.Format; any other
value is ERR_UNSUPPORTED_VERSION at step 2.
- CONTROL_CBOR has the schema version of its format. Version 2 adds key 6,
payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and
key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without
extensions, whatever L.
- The payload is the content padded with zeros to P = rule(L). Step 17
checks the length and the zeros, and Open writes only the first L bytes.
- INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials,
and a dummy in each slot left, in a uniformly random order.
Writer rules (spec §62.1): EncryptOptions.Length is required and the
source must deliver exactly that many bytes; recipients that are not
canonical or of low order are rejected (agewrap.CheckX25519Recipient);
self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE.
The CLI measures its input, takes -padding and reports the format.
Test data: seven format 2 fixtures, padding vectors checked against
math/big, format 2 CBOR vectors, and the mutation corpus in both formats
with the 22 cases of the third list of spec §64, built without randomness
by sealing the fixtures again with their known keys and nonces. The
format 1 fixtures are kept byte for byte and never regenerated; the
differential corpus keeps its 1825 cases and adds a block per format 2
fixture. The spec copy loses its "to be implemented" markers, and the
READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// Spec §70: the format is reported, with what format 1 reveals.
format1 := ! strings . HasPrefix ( tc . name , "format2_" )
if strings . Contains ( stderr , "format 1 does not hide" ) != format1 {
t . Fatalf ( "report:\n%s" , stderr )
}
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
got , _ := os . ReadFile ( out )
want , _ := os . ReadFile ( filepath . Join ( fixtures , tc . name + ".plaintext" ) )
if ! bytes . Equal ( got , want ) {
t . Fatal ( "plaintext differs" )
}
} )
}
}
Format 3, step 6b: the nine fixtures of format 3
The fixtures of spec 67 for format 3, each with its record, its BODY,
its inspect output and, for time_and_key, its .dkk:
- format3_single, format3_tree (five files in three folders, one over
two STREAM chunks, one without mtime, a comment and a declared
author), format3_comment_only (no files; a TAB in the comment),
format3_bloque256 and format3_time_and_key_portable, written with
EncryptFiles;
- format3_area_1024, format3_security_v2 (verdict X),
format3_signature_unsupported (an author-signature of alg 1 with a
random key of 32 bytes and a random signature of 64: F1) and
format3_seal_unsupported (that and a seal of seal_type 1: F1 and S1),
which only a generator of test vectors writes (62.1 rule 13), built
with testkit.Build.
The record of a format 3 fixture adds the area, SECURITY_CBOR,
HEAD_CBOR, the salt, the comment, the declared author, the head
extensions, the offset of CONTENT in BODY, each file with its layout,
SHA-256 and mtime, and the verdicts with their lines; its plaintext
file is BODY. The generator writes, then recovers every value by
opening layer by layer for the three formats alike, and refreshes the
records of format 3 through a Sink.
Tests: the conformance test checks BODY, the head, security and every
file, and opens through a MemorySink; the .dkk tests take the .dkk of
formats 2 and 3 too (spec 68); the CLI decrypts five of the fixtures
into folders; the control fuzz target decodes with the three schema
versions. The differential corpus gains two bases, format3_single and
format3_time_and_key_portable, one per policy: 5110 cases, the earlier
ones unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// Spec §56, §67: the format 3 fixtures open through the CLI into a new
// folder, with their mtimes, and the CLI shows their verdicts first and
// last (spec §29.7).
func TestDecryptFormat3Fixtures ( t * testing . T ) {
url := relay ( t )
for _ , tc := range [ ] struct { name , dkk string } {
{ "format3_single" , "" } ,
{ "format3_tree" , "" } ,
{ "format3_comment_only" , "" } ,
{ "format3_time_and_key_portable" , "format3_time_and_key_portable.dkk" } ,
{ "format3_seal_unsupported" , "" } ,
{ "format3_unsigned" , "" } ,
Signature plan, step 6: the fixture format3_signed and the vectors of its signature
format3_signed is written by EncryptFiles with a test key. Its record gives
the seed of the key, control_commit, head_digest, signers_digest,
AUTHOR_MESSAGE with its code, the signature and the content of key 2, and
verdicts carries the dkauthor1 key. The conformance test recomputes all of
it from the control, the head and the security area, and signs again from
the seed. A second test changes the context, a bit of the signature or of
the key, and the key itself, and removes the signature.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
{ "format3_signed" , "" } ,
Test data for the second implementation: alg 2, the seal and the locator
Fixtures format3_signed_cms (alg 2, two certificates, each sealed, F6) and
format3_sealed (alg 1 and a seal of seal_type 2, F4 and S4), with the
certificates, SIGNERS, the commitments, SEAL_SUBJECT, the token and the
result of each signer in their records. vectors/security_cms.json has 22
frozen areas with their context and verdicts (F1, F2, F5, F6, S1 to S5), and
vectors/locator.json the extension datekeys.capsule with its envelope hidden
in a host, its locator sealed with tlock, the padding at the boundaries and
the rules of the addresses. The README of testdata describes all of it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
6 days ago
{ "format3_signed_cms" , "" } ,
{ "format3_sealed" , "" } ,
{ "format3_note" , "" } ,
Format 3, step 6b: the nine fixtures of format 3
The fixtures of spec 67 for format 3, each with its record, its BODY,
its inspect output and, for time_and_key, its .dkk:
- format3_single, format3_tree (five files in three folders, one over
two STREAM chunks, one without mtime, a comment and a declared
author), format3_comment_only (no files; a TAB in the comment),
format3_bloque256 and format3_time_and_key_portable, written with
EncryptFiles;
- format3_area_1024, format3_security_v2 (verdict X),
format3_signature_unsupported (an author-signature of alg 1 with a
random key of 32 bytes and a random signature of 64: F1) and
format3_seal_unsupported (that and a seal of seal_type 1: F1 and S1),
which only a generator of test vectors writes (62.1 rule 13), built
with testkit.Build.
The record of a format 3 fixture adds the area, SECURITY_CBOR,
HEAD_CBOR, the salt, the comment, the declared author, the head
extensions, the offset of CONTENT in BODY, each file with its layout,
SHA-256 and mtime, and the verdicts with their lines; its plaintext
file is BODY. The generator writes, then recovers every value by
opening layer by layer for the three formats alike, and refreshes the
records of format 3 through a Sink.
Tests: the conformance test checks BODY, the head, security and every
file, and opens through a MemorySink; the .dkk tests take the .dkk of
formats 2 and 3 too (spec 68); the CLI decrypts five of the fixtures
into folders; the control fuzz target decodes with the three schema
versions. The differential corpus gains two bases, format3_single and
format3_time_and_key_portable, one per policy: 5110 cases, the earlier
ones unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
} {
t . Run ( tc . name , func ( t * testing . T ) {
var f testkit . DKCFixture
if err := testkit . ReadJSON ( filepath . Join ( fixtures , tc . name + ".json" ) , & f ) ; err != nil {
t . Fatal ( err )
}
body , err := os . ReadFile ( filepath . Join ( fixtures , f . PlaintextFile ) )
if err != nil {
t . Fatal ( err )
}
out := filepath . Join ( t . TempDir ( ) , "out" )
args := [ ] string { "decrypt" , "-in" , filepath . Join ( fixtures , f . File ) , "-out" , out , "-relay" , url }
if tc . dkk != "" {
args = append ( args , "-dkk" , filepath . Join ( fixtures , tc . dkk ) )
}
stdout , stderr , err := cli ( t , later , args ... )
if err != nil {
t . Fatalf ( "%v\n%s" , err , stderr )
}
for _ , file := range f . Files {
name := filepath . Join ( out , filepath . FromSlash ( file . Path ) )
got , err := os . ReadFile ( name )
if err != nil || ! bytes . Equal ( got , body [ f . ContentOffset + file . Start : f . ContentOffset + file . End ] ) {
t . Errorf ( "%s: %v" , file . Path , err )
}
if info , err := os . Stat ( name ) ; file . MTime != nil && ( err != nil || info . ModTime ( ) . Unix ( ) != int64 ( * file . MTime ) ) {
t . Errorf ( "%s: mtime %v" , file . Path , err )
}
}
if _ , err := os . Lstat ( out ) ; len ( f . Files ) == 0 && ! errors . Is ( err , os . ErrNotExist ) {
t . Error ( "a capsule without files created its folder" )
}
Review fixes: author keys, the writer, the CLI, extensions and the locator
Fixes of the review of the session of 1 and 2 October that the text of
spec v0.11 already asks for:
- authorkey: String and GoString hide the secret key, which only Secret
returns; ParsePublic refuses a key that is not a point of the curve
(ed25519strict.OnCurve, checked against the square root of testkit).
- capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never
a capsule without the signature or the seal that was asked for. A panic
while evaluating the signature or the seal fails only that part, F1 or
S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can
refuse to publish the files.
- extension.CheckWrite, the rule of encoders of spec 72: the writers of
capsules and .dkk files refuse datekeys.note and datekeys.capsule outside
the arrays where they are registered, or with invalid data.
- CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE
before it signs (rule 20); decrypt -expect-author compares the key of an
F4 and writes nothing unless it matches; decrypt notifies a public note
that it does not show; the lines of the verdicts break at the last space
that fits, each row after the first behind a mark, so that the terminal
never breaks them; L is the payload, not the content.
- locator: a reader rejects an address that breaks 44.1 and keeps the
others; addresses refuse the special-purpose blocks of IANA, IPv6 outside
2000::/3, localhost and local names, characters outside RFC 3986, dot
segments, and a CID that does not decode to version 1 and a multihash;
ParseInfo checks that the locator is an age file with one tlock stanza
for the round of its DateKey; Info.Extension reads what it writes; its
errors carry no normative code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
lines := strings . Split ( strings . TrimSuffix ( joined ( stdout ) , "\n" ) , "\n" )
Format 3, step 6b: the nine fixtures of format 3
The fixtures of spec 67 for format 3, each with its record, its BODY,
its inspect output and, for time_and_key, its .dkk:
- format3_single, format3_tree (five files in three folders, one over
two STREAM chunks, one without mtime, a comment and a declared
author), format3_comment_only (no files; a TAB in the comment),
format3_bloque256 and format3_time_and_key_portable, written with
EncryptFiles;
- format3_area_1024, format3_security_v2 (verdict X),
format3_signature_unsupported (an author-signature of alg 1 with a
random key of 32 bytes and a random signature of 64: F1) and
format3_seal_unsupported (that and a seal of seal_type 1: F1 and S1),
which only a generator of test vectors writes (62.1 rule 13), built
with testkit.Build.
The record of a format 3 fixture adds the area, SECURITY_CBOR,
HEAD_CBOR, the salt, the comment, the declared author, the head
extensions, the offset of CONTENT in BODY, each file with its layout,
SHA-256 and mtime, and the verdicts with their lines; its plaintext
file is BODY. The generator writes, then recovers every value by
opening layer by layer for the three formats alike, and refreshes the
records of format 3 through a Sink.
Tests: the conformance test checks BODY, the head, security and every
file, and opens through a MemorySink; the .dkk tests take the .dkk of
formats 2 and 3 too (spec 68); the CLI decrypts five of the fixtures
into folders; the control fuzz target decodes with the three schema
versions. The differential corpus gains two bases, format3_single and
format3_time_and_key_portable, one per policy: 5110 cases, the earlier
ones unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
n := len ( f . Verdicts . Lines )
if len ( lines ) < 2 * n || ! reflect . DeepEqual ( lines [ : n ] , f . Verdicts . Lines ) || ! reflect . DeepEqual ( lines [ len ( lines ) - n : ] , f . Verdicts . Lines ) {
t . Errorf ( "the verdicts are not first and last:\n%s" , stdout )
}
} )
}
}
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
func TestDecryptWithIdentityFile ( t * testing . T ) {
var f testkit . DKCFixture
if err := testkit . ReadJSON ( filepath . Join ( fixtures , "time_and_key_recipients.json" ) , & f ) ; err != nil {
t . Fatal ( err )
}
dir := t . TempDir ( )
key := filepath . Join ( dir , "key.txt" )
os . WriteFile ( key , [ ] byte ( "# test identity\n" + f . Identities [ 1 ] + "\n" ) , 0 o600 )
out := filepath . Join ( dir , "plain" )
if _ , stderr , err := cli ( t , later , "decrypt" , "-in" , filepath . Join ( fixtures , f . File ) , "-out" , out , "-identity" , key , "-relay" , relay ( t ) ) ; err != nil {
t . Fatalf ( "%v\n%s" , err , stderr )
}
}
func TestDecryptFailuresLeaveNothing ( t * testing . T ) {
dir := t . TempDir ( )
b , _ := os . ReadFile ( filepath . Join ( fixtures , "time_only.dkc" ) )
bad := filepath . Join ( dir , "bad.dkc" )
b [ len ( b ) - 1 ] ^ = 1
os . WriteFile ( bad , b , 0 o600 )
out := filepath . Join ( dir , "plain" )
_ , _ , err := cli ( t , later , "decrypt" , "-in" , bad , "-out" , out , "-relay" , relay ( t ) )
if ! errors . Is ( err , datekeys . ErrIntegrity ) {
t . Fatalf ( "got %v" , err )
}
if entries , _ := os . ReadDir ( dir ) ; len ( entries ) != 1 {
t . Fatalf ( "left files behind: %v" , entries )
}
// time_and_key without credentials fails before contacting any relay.
_ , _ , err = cli ( t , later , "decrypt" , "-in" , filepath . Join ( fixtures , "time_and_key_portable.dkc" ) , "-out" , out , "-relay" , "http://127.0.0.1:1" )
if ! errors . Is ( err , datekeys . ErrAccessRequired ) {
t . Fatalf ( "got %v" , err )
}
}
Spec v0.8.2 refinements: error precedence, trust model, strict order
Approved refinements, each recorded with its reproducible case in the
§76 v0.8.2 subsection:
- §69.1: layered error model with normative precedence (frame, type tag
and version, CBOR profile and CDDL, then fields with their own code in
ascending key order; across steps the §63 order decides), with a scope
paragraph for the optional steps 5, 6 and 8.
- §55.1: normative trust table per section (who can write it, from which
step it is bound, what it never proves); §72: security-relevant claims
go in CONTROL_CBOR or under a signature, .dkk data is advisory.
- §31/§54: extension arrays in strictly ascending unsigned byte order of
extension_id (one rule for order and uniqueness).
- Gaps a second implementation needed: §28.1 malformed age headers,
§15/§19 latest unlock time and dk1_ reading rules, §22/§23/§57 length
lower bounds, §63 step 8 tlock argument comparison and step 9 order,
§12.1 profile validation with the drand chain-hash formula, §74 table
of implementation limits.
Reference alignment: .dkk errors only at step 9.a (new
OpenOptions.AccessKeyFile, used by the CLI), CR/LF in dk1_ is
ERR_DATEKEY_INVALID, BODY_LEN 0 is ERR_INTEGRITY, nil identities are not
credentials, and AccessIdentity tries every identity on every stanza so
its verdict does not depend on their order. dk1.json gains three
vectors; every other testdata file is byte-identical.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
// Spec §63 step 9.a, §69.1: the CLI hands the .dkk to capsule.Open still
// encoded, so its errors come at step 9 of a time_and_key capsule, after any
// failure of steps 1 to 8, and a time_only capsule ignores it.
func TestDecryptAccessKeyOrder ( t * testing . T ) {
dir := t . TempDir ( )
notDKK := filepath . Join ( dir , "not.dkk" )
os . WriteFile ( notDKK , [ ] byte ( "not a .dkk" ) , 0 o600 )
b , _ := os . ReadFile ( filepath . Join ( fixtures , "time_and_key_portable.dkc" ) )
b [ 5 ] = 1
flags := filepath . Join ( dir , "flags.dkc" )
os . WriteFile ( flags , b , 0 o600 )
for _ , tc := range [ ] struct {
name , in string
want error
} {
{ "time_and_key and bytes that are not a .dkk" , filepath . Join ( fixtures , "time_and_key_portable.dkc" ) , datekeys . ErrInvalidMagic } ,
{ "FLAGS 1 and bytes that are not a .dkk" , flags , datekeys . ErrInvalidFlags } ,
} {
_ , _ , err := cli ( t , later , "decrypt" , "-in" , tc . in , "-out" , filepath . Join ( dir , "plain" ) , "-dkk" , notDKK , "-relay" , "http://127.0.0.1:1" )
if ! errors . Is ( err , tc . want ) {
t . Errorf ( "%s: got %v, want %s" , tc . name , err , datekeys . Code ( tc . want ) )
}
}
out := filepath . Join ( dir , "plain" )
if _ , stderr , err := cli ( t , later , "decrypt" , "-in" , filepath . Join ( fixtures , "time_only.dkc" ) , "-out" , out , "-dkk" , notDKK , "-relay" , relay ( t ) ) ; err != nil {
t . Fatalf ( "time_only and bytes that are not a .dkk: %v\n%s" , err , stderr )
}
}
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
func TestEncryptDecryptRoundTrip ( t * testing . T ) {
dir := t . TempDir ( )
in := filepath . Join ( dir , "secret.txt" )
os . WriteFile ( in , [ ] byte ( "round trip through the CLI" ) , 0 o600 )
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
fotos := filepath . Join ( dir , "fotos" )
os . MkdirAll ( filepath . Join ( fotos , "sub" ) , 0 o700 )
os . MkdirAll ( filepath . Join ( fotos , "__MACOSX" ) , 0 o700 )
os . WriteFile ( filepath . Join ( fotos , "a.jpg" ) , [ ] byte ( "jpeg" ) , 0 o600 )
os . WriteFile ( filepath . Join ( fotos , "sub" , "b.txt" ) , [ ] byte ( "b" ) , 0 o600 )
os . WriteFile ( filepath . Join ( fotos , ".DS_Store" ) , [ ] byte ( "x" ) , 0 o600 )
os . WriteFile ( filepath . Join ( fotos , "__MACOSX" , "._a.jpg" ) , [ ] byte ( "x" ) , 0 o600 )
old := time . Date ( 2020 , 1 , 2 , 3 , 4 , 5 , 0 , time . UTC )
os . Chtimes ( in , old , old )
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
p := profile . Quicknet ( )
unlock := time . Unix ( p . GenesisTime + 999 * 3 , 0 ) . UTC ( ) // round 1000
genesis := time . Unix ( p . GenesisTime , 0 )
x , _ := age . GenerateX25519Identity ( )
key := filepath . Join ( dir , "x.txt" )
os . WriteFile ( key , [ ] byte ( x . String ( ) + "\n" ) , 0 o600 )
dkc , dkk := filepath . Join ( dir , "s.dkc" ) , filepath . Join ( dir , "s.dkk" )
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
_ , stderr , err := cli ( t , genesis , "encrypt" , "-at" , unlock . Format ( time . RFC3339 ) , "-in" , in , "-in" , fotos , "-out" , dkc ,
"-comment" , "Hola\tmundo" , "-author" , "Ana" , "-policy" , "time_and_key" , "-recipient" , x . Recipient ( ) . String ( ) , "-dkk" , dkk )
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
if err != nil {
t . Fatalf ( "%v\n%s" , err , stderr )
}
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
if ! strings . Contains ( stderr , "round 1000" ) || ! strings . Contains ( stderr , "format 3: 3 files, " ) ||
! strings . Contains ( stderr , ".DS_Store, which the system creates on its own" ) || ! strings . Contains ( stderr , "__MACOSX, which the system" ) ||
Implement capsule format 2 of spec v0.9
The reference moves to the DateKeys Protocol Specification v0.9, approved
by its author on 29 September 2026. Encrypt writes capsule format 2 only;
Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the
verdict v0.8.2 gave it.
Format 2 (spec §22, §29.1, §31, §39):
- VERSION in the PRELUDE is the capsule format, capsule.Format; any other
value is ERR_UNSUPPORTED_VERSION at step 2.
- CONTROL_CBOR has the schema version of its format. Version 2 adds key 6,
payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and
key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without
extensions, whatever L.
- The payload is the content padded with zeros to P = rule(L). Step 17
checks the length and the zeros, and Open writes only the first L bytes.
- INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials,
and a dummy in each slot left, in a uniformly random order.
Writer rules (spec §62.1): EncryptOptions.Length is required and the
source must deliver exactly that many bytes; recipients that are not
canonical or of low order are rejected (agewrap.CheckX25519Recipient);
self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE.
The CLI measures its input, takes -padding and reports the format.
Test data: seven format 2 fixtures, padding vectors checked against
math/big, format 2 CBOR vectors, and the mutation corpus in both formats
with the 22 cases of the third list of spec §64, built without randomness
by sealing the fixtures again with their known keys and nonces. The
format 1 fixtures are kept byte for byte and never regenerated; the
differential corpus keeps its 1825 cases and adds a block per format 2
fixture. The spec copy loses its "to be implemented" markers, and the
READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
strings . Contains ( stderr , "not post-quantum" ) {
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
t . Fatalf ( "unexpected report:\n%s" , stderr )
}
if _ , _ , err := cli ( t , genesis , "encrypt" , "-at" , unlock . Format ( time . RFC3339 ) , "-in" , in , "-out" , dkc ) ; err == nil {
t . Fatal ( "overwrote an existing capsule" )
}
Implement capsule format 2 of spec v0.9
The reference moves to the DateKeys Protocol Specification v0.9, approved
by its author on 29 September 2026. Encrypt writes capsule format 2 only;
Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the
verdict v0.8.2 gave it.
Format 2 (spec §22, §29.1, §31, §39):
- VERSION in the PRELUDE is the capsule format, capsule.Format; any other
value is ERR_UNSUPPORTED_VERSION at step 2.
- CONTROL_CBOR has the schema version of its format. Version 2 adds key 6,
payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and
key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without
extensions, whatever L.
- The payload is the content padded with zeros to P = rule(L). Step 17
checks the length and the zeros, and Open writes only the first L bytes.
- INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials,
and a dummy in each slot left, in a uniformly random order.
Writer rules (spec §62.1): EncryptOptions.Length is required and the
source must deliver exactly that many bytes; recipients that are not
canonical or of low order are rejected (agewrap.CheckX25519Recipient);
self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE.
The CLI measures its input, takes -padding and reports the format.
Test data: seven format 2 fixtures, padding vectors checked against
math/big, format 2 CBOR vectors, and the mutation corpus in both formats
with the 22 cases of the third list of spec §64, built without randomness
by sealing the fixtures again with their known keys and nonces. The
format 1 fixtures are kept byte for byte and never regenerated; the
differential corpus keeps its 1825 cases and adds a block per format 2
fixture. The spec copy loses its "to be implemented" markers, and the
READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// Spec §29.1: the padding rule is one of the two, never none.
small := filepath . Join ( dir , "small.dkc" )
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
if _ , stderr , err := cli ( t , genesis , "encrypt" , "-at" , unlock . Format ( time . RFC3339 ) , "-in" , in , "-out" , small , "-padding" , "bloque256" , "-no-mtime" ) ; err != nil ||
Implement capsule format 2 of spec v0.9
The reference moves to the DateKeys Protocol Specification v0.9, approved
by its author on 29 September 2026. Encrypt writes capsule format 2 only;
Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the
verdict v0.8.2 gave it.
Format 2 (spec §22, §29.1, §31, §39):
- VERSION in the PRELUDE is the capsule format, capsule.Format; any other
value is ERR_UNSUPPORTED_VERSION at step 2.
- CONTROL_CBOR has the schema version of its format. Version 2 adds key 6,
payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and
key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without
extensions, whatever L.
- The payload is the content padded with zeros to P = rule(L). Step 17
checks the length and the zeros, and Open writes only the first L bytes.
- INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials,
and a dummy in each slot left, in a uniformly random order.
Writer rules (spec §62.1): EncryptOptions.Length is required and the
source must deliver exactly that many bytes; recipients that are not
canonical or of low order are rejected (agewrap.CheckX25519Recipient);
self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE.
The CLI measures its input, takes -padding and reports the format.
Test data: seven format 2 fixtures, padding vectors checked against
math/big, format 2 CBOR vectors, and the mutation corpus in both formats
with the 22 cases of the third list of spec §64, built without randomness
by sealing the fixtures again with their known keys and nonces. The
format 1 fixtures are kept byte for byte and never regenerated; the
differential corpus keeps its 1825 cases and adds a block per format 2
fixture. The spec copy loses its "to be implemented" markers, and the
READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
! strings . Contains ( stderr , "(bloque256)" ) {
t . Fatalf ( "-padding bloque256: %v\n%s" , err , stderr )
}
if _ , _ , err := cli ( t , genesis , "encrypt" , "-at" , unlock . Format ( time . RFC3339 ) , "-in" , in , "-out" , filepath . Join ( dir , "none.dkc" ) , "-padding" , "none" ) ; err == nil {
t . Fatal ( "encrypted without a padding rule" )
}
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
stdout , _ , err := cli ( t , later , "inspect" , "-in" , dkc , "-json" )
if err != nil {
t . Fatal ( err )
}
var v inspectview . View
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
if err := json . Unmarshal ( [ ] byte ( stdout ) , & v ) ; err != nil || ! v . Valid || v . Round != 1000 || v . AccessPolicy != "time_and_key" || v . Format != 3 {
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
t . Fatalf ( "inspect: %+v %v" , v , err )
}
for i , extra := range [ ] [ ] string { { "-dkk" , dkk } , { "-identity" , key } } {
out := filepath . Join ( dir , fmt . Sprintf ( "out%d" , i ) )
args := append ( [ ] string { "decrypt" , "-in" , dkc , "-out" , out , "-relay" , relay ( t ) } , extra ... )
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
stdout , stderr , err := cli ( t , later , args ... )
if err != nil {
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
t . Fatalf ( "%v\n%s" , err , stderr )
}
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
for name , want := range map [ string ] string { "secret.txt" : "round trip through the CLI" , "fotos/a.jpg" : "jpeg" , "fotos/sub/b.txt" : "b" } {
if b , _ := os . ReadFile ( filepath . Join ( out , filepath . FromSlash ( name ) ) ) ; string ( b ) != want {
t . Errorf ( "%s: %q" , name , b )
}
}
if entries , _ := os . ReadDir ( out ) ; len ( entries ) != 2 {
t . Errorf ( "%d entries in the folder, want fotos and secret.txt" , len ( entries ) )
}
if info , err := os . Stat ( filepath . Join ( out , "secret.txt" ) ) ; err != nil || ! info . ModTime ( ) . Equal ( old ) {
t . Errorf ( "mtime of secret.txt: %v" , err )
}
// Spec §29.7: the verdicts, the declared author and the comment, as
// text of the creator, the paths, and the verdicts again.
want := "Sin firma de autor.\n" + authorLabel + "\n│ Ana\n┌ " + commentTitle + "\n│ Hola mundo\n└\n" +
"Ficheros escritos en " + out + " (3):\n│ fotos/a.jpg\n│ fotos/sub/b.txt\n│ secret.txt\nSin firma de autor.\n"
if stdout != want {
t . Errorf ( "presentation:\n%s\nwant:\n%s" , stdout , want )
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
}
}
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// The folder exists: nothing is requested and nothing changes.
out := filepath . Join ( dir , "out0" )
if _ , _ , err := cli ( t , later , "decrypt" , "-in" , dkc , "-out" , out , "-dkk" , dkk , "-relay" , "http://127.0.0.1:1" ) ; err == nil || ! strings . Contains ( err . Error ( ) , "already exists" ) {
t . Fatalf ( "decrypted into an existing folder: %v" , err )
}
// -no-mtime: the file gets the time of its extraction.
out = filepath . Join ( dir , "small" )
if _ , stderr , err := cli ( t , later , "decrypt" , "-in" , small , "-out" , out , "-relay" , relay ( t ) ) ; err != nil {
t . Fatalf ( "%v\n%s" , err , stderr )
}
if info , err := os . Stat ( filepath . Join ( out , "secret.txt" ) ) ; err != nil || info . ModTime ( ) . Equal ( old ) {
t . Errorf ( "-no-mtime kept the mtime: %v" , err )
}
}
// Spec §56: a format 3 capsule that fails leaves no folder, and one without
// files creates none.
func TestDecryptFormat3LeavesNothing ( t * testing . T ) {
dir := t . TempDir ( )
p := profile . Quicknet ( )
unlock := time . Unix ( p . GenesisTime + 999 * 3 , 0 ) . UTC ( )
genesis := time . Unix ( p . GenesisTime , 0 )
in := filepath . Join ( dir , "a.txt" )
os . WriteFile ( in , [ ] byte ( "a" ) , 0 o600 )
dkc , note := filepath . Join ( dir , "a.dkc" ) , filepath . Join ( dir , "note.dkc" )
if _ , stderr , err := cli ( t , genesis , "encrypt" , "-at" , unlock . Format ( time . RFC3339 ) , "-in" , in , "-out" , dkc ) ; err != nil {
t . Fatalf ( "%v\n%s" , err , stderr )
}
if _ , stderr , err := cli ( t , genesis , "encrypt" , "-at" , unlock . Format ( time . RFC3339 ) , "-comment" , "Solo un comentario" , "-out" , note ) ; err != nil {
t . Fatalf ( "%v\n%s" , err , stderr )
}
b , _ := os . ReadFile ( dkc )
b [ len ( b ) - 1 ] ^ = 1
bad := filepath . Join ( dir , "bad.dkc" )
os . WriteFile ( bad , b , 0 o600 )
out := filepath . Join ( dir , "out" )
if _ , _ , err := cli ( t , later , "decrypt" , "-in" , bad , "-out" , out , "-relay" , relay ( t ) ) ; ! errors . Is ( err , datekeys . ErrIntegrity ) {
t . Fatalf ( "got %v" , err )
}
if _ , err := os . Lstat ( out ) ; ! errors . Is ( err , os . ErrNotExist ) {
t . Fatalf ( "the folder of a failed capsule remains: %v" , err )
}
// A folder whose parent does not exist fails before any request: the
// relay cannot be reached.
missing := filepath . Join ( dir , "missing" , "out" )
if _ , _ , err := cli ( t , later , "decrypt" , "-in" , dkc , "-out" , missing , "-relay" , "http://127.0.0.1:1" ) ; err == nil || ! strings . Contains ( err . Error ( ) , "is not a folder" ) {
t . Fatalf ( "a folder without its parent: %v" , err )
}
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
stdout , stderr , err := cli ( t , later , "decrypt" , "-in" , note , "-out" , out , "-relay" , relay ( t ) )
if err != nil || ! strings . Contains ( stdout , "│ Solo un comentario" ) || ! strings . Contains ( stderr , "no files" ) {
t . Fatalf ( "%v\n%s\n%s" , err , stdout , stderr )
}
if _ , err := os . Lstat ( out ) ; ! errors . Is ( err , os . ErrNotExist ) {
t . Fatalf ( "a capsule without files created its folder: %v" , err )
}
}
// Spec §62.1 rule 15: a path that breaks a rule is refused with the rule and
// the character, and folders are walked without following links.
func TestEncryptRefusesPaths ( t * testing . T ) {
dir := t . TempDir ( )
p := profile . Quicknet ( )
at := time . Unix ( p . GenesisTime + 999 * 3 , 0 ) . UTC ( ) . Format ( time . RFC3339 )
genesis := time . Unix ( p . GenesisTime , 0 )
bidi := filepath . Join ( dir , "a\u202eb.txt" )
if err := os . WriteFile ( bidi , [ ] byte ( "x" ) , 0 o600 ) ; err != nil {
t . Fatal ( err )
}
_ , _ , err := cli ( t , genesis , "encrypt" , "-at" , at , "-in" , bidi , "-out" , filepath . Join ( dir , "1.dkc" ) )
if err == nil || ! strings . Contains ( err . Error ( ) , "R4: segment 1: invisible U+202E" ) {
t . Fatalf ( "got %v" , err )
}
linked := filepath . Join ( dir , "linked" )
os . Mkdir ( linked , 0 o700 )
if err := os . Symlink ( bidi , filepath . Join ( linked , "link" ) ) ; err != nil {
t . Skipf ( "no symbolic links here: %v" , err )
}
if _ , _ , err := cli ( t , genesis , "encrypt" , "-at" , at , "-in" , linked , "-out" , filepath . Join ( dir , "2.dkc" ) ) ; err == nil || ! strings . Contains ( err . Error ( ) , "is not a regular file" ) {
t . Fatalf ( "got %v" , err )
}
}
// Spec §29.7: every line of the creator goes in pieces of at most W - 3
// columns behind the prefix, counting 2 for any code point that is not
// printable ASCII; TABs of the comment go to the next multiple of 8; the
Review fixes: author keys, the writer, the CLI, extensions and the locator
Fixes of the review of the session of 1 and 2 October that the text of
spec v0.11 already asks for:
- authorkey: String and GoString hide the secret key, which only Secret
returns; ParsePublic refuses a key that is not a point of the curve
(ed25519strict.OnCurve, checked against the square root of testkit).
- capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never
a capsule without the signature or the seal that was asked for. A panic
while evaluating the signature or the seal fails only that part, F1 or
S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can
refuse to publish the files.
- extension.CheckWrite, the rule of encoders of spec 72: the writers of
capsules and .dkk files refuse datekeys.note and datekeys.capsule outside
the arrays where they are registered, or with invalid data.
- CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE
before it signs (rule 20); decrypt -expect-author compares the key of an
F4 and writes nothing unless it matches; decrypt notifies a public note
that it does not show; the lines of the verdicts break at the last space
that fits, each row after the first behind a mark, so that the terminal
never breaks them; L is the payload, not the content.
- locator: a reader rejects an address that breaks 44.1 and keeps the
others; addresses refuse the special-purpose blocks of IANA, IPv6 outside
2000::/3, localhost and local names, characters outside RFC 3986, dot
segments, and a CID that does not decode to version 1 and a multihash;
ParseInfo checks that the locator is an age file with one tlock stanza
for the round of its DateKey; Info.Extension reads what it writes; its
errors carry no normative code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
// verdicts come first and last, in rows of at most W - 3 columns; risky names
// get a warning.
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
func TestPresent ( t * testing . T ) {
o := & capsule . Opened {
Verdicts : capsule . Verdicts { Signature : capsule . VerdictUnreadable , Seal : capsule . VerdictUnreadable } ,
Head : & capsule . Head {
Author : strings . Repeat ( "ñ" , 12 ) ,
Comment : "a\tb\n\n" + strings . Repeat ( "x" , 40 ) ,
Files : [ ] capsule . File { { Path : "Informe.LNK" } , { Path : ".GIT/config" } , { Path : "-rf" } , { Path : "setup.Exe" } , { Path : "fotos/Desktop.ini" } , { Path : "nota.txt" } } ,
} ,
}
var b bytes . Buffer
present ( & b , o , "DIR" , 20 )
Review fixes: author keys, the writer, the CLI, extensions and the locator
Fixes of the review of the session of 1 and 2 October that the text of
spec v0.11 already asks for:
- authorkey: String and GoString hide the secret key, which only Secret
returns; ParsePublic refuses a key that is not a point of the curve
(ed25519strict.OnCurve, checked against the square root of testkit).
- capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never
a capsule without the signature or the seal that was asked for. A panic
while evaluating the signature or the seal fails only that part, F1 or
S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can
refuse to publish the files.
- extension.CheckWrite, the rule of encoders of spec 72: the writers of
capsules and .dkk files refuse datekeys.note and datekeys.capsule outside
the arrays where they are registered, or with invalid data.
- CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE
before it signs (rule 20); decrypt -expect-author compares the key of an
F4 and writes nothing unless it matches; decrypt notifies a public note
that it does not show; the lines of the verdicts break at the last space
that fits, each row after the first behind a mark, so that the terminal
never breaks them; L is the payload, not the content.
- locator: a reader rejects an address that breaks 44.1 and keeps the
others; addresses refuse the special-purpose blocks of IANA, IPv6 outside
2000::/3, localhost and local names, characters outside RFC 3986, dot
segments, and a CID that does not decode to version 1 and a multihash;
ParseInfo checks that the locator is an age file with one tlock stanza
for the round of its DateKey; Info.Extension reads what it writes; its
errors carry no normative code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
out := joined ( b . String ( ) )
lines := strings . Split ( strings . TrimSuffix ( out , "\n" ) , "\n" )
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
x := capsule . VerdictUnreadable . Text ( )
if lines [ 0 ] != x || lines [ len ( lines ) - 1 ] != x {
t . Errorf ( "the verdict is not first and last:\n%s" , b . String ( ) )
}
Review fixes: author keys, the writer, the CLI, extensions and the locator
Fixes of the review of the session of 1 and 2 October that the text of
spec v0.11 already asks for:
- authorkey: String and GoString hide the secret key, which only Secret
returns; ParsePublic refuses a key that is not a point of the curve
(ed25519strict.OnCurve, checked against the square root of testkit).
- capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never
a capsule without the signature or the seal that was asked for. A panic
while evaluating the signature or the seal fails only that part, F1 or
S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can
refuse to publish the files.
- extension.CheckWrite, the rule of encoders of spec 72: the writers of
capsules and .dkk files refuse datekeys.note and datekeys.capsule outside
the arrays where they are registered, or with invalid data.
- CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE
before it signs (rule 20); decrypt -expect-author compares the key of an
F4 and writes nothing unless it matches; decrypt notifies a public note
that it does not show; the lines of the verdicts break at the last space
that fits, each row after the first behind a mark, so that the terminal
never breaks them; L is the payload, not the content.
- locator: a reader rejects an address that breaks 44.1 and keeps the
others; addresses refuse the special-purpose blocks of IANA, IPv6 outside
2000::/3, localhost and local names, characters outside RFC 3986, dot
segments, and a CID that does not decode to version 1 and a multihash;
ParseInfo checks that the locator is an age file with one tlock stanza
for the round of its DateKey; Info.Extension reads what it writes; its
errors carry no normative code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
verdictRows := strings . Count ( b . String ( ) , contMark ) / 2
for i , l := range strings . Split ( strings . TrimSuffix ( b . String ( ) , "\n" ) , "\n" ) {
w := 0
for _ , r := range l {
w += runeWidth ( r )
}
rest , creator := strings . CutPrefix ( l , prefix )
switch {
case creator && ( w > 20 || rest == "" && l != prefix ) :
t . Errorf ( "piece %q of %d columns" , rest , w )
case i <= verdictRows && w > 20 - prefixWidth :
t . Errorf ( "row %q of the verdicts of %d columns" , l , w )
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
}
}
for _ , want := range [ ] string {
"│ ññññññññ\n│ ññññ\n" , // 12 × 2 columns in pieces of 16
"│ a b\n│ \n│ xxxxxxxxxxxxxxxxx\n" , // the TAB to column 8, an empty line
"│ Informe.LNK\n aviso: es un acceso directo de Windows" ,
"│ .GIT/config\n aviso: está dentro de una carpeta .git" ,
"│ -rf\n aviso: un nombre que empieza por '-'" ,
"│ setup.Exe\n aviso: es un programa o un script" ,
"│ fotos/Desktop.ini\n aviso: es la configuración de una carpeta de Windows" ,
"│ nota.txt\n" + x ,
} {
Review fixes: author keys, the writer, the CLI, extensions and the locator
Fixes of the review of the session of 1 and 2 October that the text of
spec v0.11 already asks for:
- authorkey: String and GoString hide the secret key, which only Secret
returns; ParsePublic refuses a key that is not a point of the curve
(ed25519strict.OnCurve, checked against the square root of testkit).
- capsule: a typed nil in AuthorKey, CMSSigner or Sealer is an error, never
a capsule without the signature or the seal that was asked for. A panic
while evaluating the signature or the seal fails only that part, F1 or
S2, not both. OpenOptions.Accept sees the verdicts before step 18 and can
refuse to publish the files.
- extension.CheckWrite, the rule of encoders of spec 72: the writers of
capsules and .dkk files refuse datekeys.note and datekeys.capsule outside
the arrays where they are registered, or with invalid data.
- CLI: encrypt -sign shows the author key and the code of AUTHOR_MESSAGE
before it signs (rule 20); decrypt -expect-author compares the key of an
F4 and writes nothing unless it matches; decrypt notifies a public note
that it does not show; the lines of the verdicts break at the last space
that fits, each row after the first behind a mark, so that the terminal
never breaks them; L is the payload, not the content.
- locator: a reader rejects an address that breaks 44.1 and keeps the
others; addresses refuse the special-purpose blocks of IANA, IPv6 outside
2000::/3, localhost and local names, characters outside RFC 3986, dot
segments, and a CID that does not decode to version 1 and a multihash;
ParseInfo checks that the locator is an age file with one tlock stanza
for the round of its DateKey; Info.Extension reads what it writes; its
errors carry no normative code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6 days ago
if ! strings . Contains ( out , want ) {
t . Errorf ( "missing %q in:\n%s" , want , out )
Format 3, step 5: the CLI
datekeys encrypt writes format 3 and datekeys decrypt writes its files
to a new folder, with the presentation of spec 29.7.
- encrypt: -in is repeatable and takes files and folders; a folder
gives its name as the first segment, as a browser does, and is
walked with Lstat, following no link, taking regular files only.
.DS_Store, Thumbs.db, desktop.ini, ._* and __MACOSX are left out of
folders, and each one left out is reported (62.1 rule 15). New
-comment, -author and -no-mtime; the mtimes are kept by default
(rule 16). A capsule may hold a comment alone. The copy of a pipe to
a temporary file goes, as only regular files are taken.
- decrypt: the prelude decides. Format 3 claims -out with os.Mkdir,
only when there are files, stages the tree in -out/.datekeys-*
through an os.Root with O_EXCL and mode 0600, sets the mtimes, and
moves each entry of the first level into place at step 18; any
failure removes the folder (spec 56). Formats 1 and 2 still write a
file.
- The presentation goes to stdout: the verdicts, the declared author
and the comment box with their labels, the paths, and the verdicts
again. Every line of the creator goes in pieces of at most W - 3
columns behind the prefix, counting 2 for anything but printable
ASCII, with its TABs expanded to multiples of 8; W is the width of
the terminal, asked with syscall on Unix and Windows, or 80. Risky
names get a warning: shortcuts, desktop.ini, .git, programs and a
leading dash, compared by their key of R7.
- Encrypt no longer runs in the CLI: only the test data generators
set TestVectors.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
}
}
if pieces ( "" , 17 ) [ 0 ] != "" || len ( pieces ( "ab" , 1 ) ) != 2 {
t . Error ( "an empty line is one piece, and every piece holds a code point" )
}
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
}
func TestInspectReportsFailures ( t * testing . T ) {
b , _ := os . ReadFile ( filepath . Join ( fixtures , "time_only.dkc" ) )
b = bytes . Replace ( b , [ ] byte ( "-> tlock 1000 " ) , [ ] byte ( "-> tlock 1001 " ) , 1 )
path := filepath . Join ( t . TempDir ( ) , "bad.dkc" )
os . WriteFile ( path , b , 0 o600 )
stdout , _ , err := cli ( t , later , "inspect" , "-in" , path )
if ! errors . Is ( err , datekeys . ErrRoundMismatch ) || ! strings . Contains ( stdout , "[FAIL] step 8" ) {
t . Fatalf ( "%v\n%s" , err , stdout )
}
stdout , _ , err = cli ( t , later , "inspect" , "-in" , filepath . Join ( fixtures , "time_only.dkc" ) )
if err != nil || ! strings . Contains ( stdout , "valid before unlock" ) {
t . Fatalf ( "%v\n%s" , err , stdout )
}
}
// The frozen inspect outputs (testdata/fixtures/<name>.inspect.json) are
// exactly what "datekeys inspect -json -in <name>.dkc" prints in the fixture
// directory.
func TestInspectJSONGoldens ( t * testing . T ) {
names , err := filepath . Glob ( filepath . Join ( fixtures , "*.inspect.json" ) )
if err != nil {
t . Fatal ( err )
}
Implement capsule format 2 of spec v0.9
The reference moves to the DateKeys Protocol Specification v0.9, approved
by its author on 29 September 2026. Encrypt writes capsule format 2 only;
Open and Inspect read formats 1 and 2, and a format 1 capsule keeps the
verdict v0.8.2 gave it.
Format 2 (spec §22, §29.1, §31, §39):
- VERSION in the PRELUDE is the capsule format, capsule.Format; any other
value is ERR_UNSUPPORTED_VERSION at step 2.
- CONTROL_CBOR has the schema version of its format. Version 2 adds key 6,
payload_length (8 bytes, big-endian, at most L_MAX = 2^53 - 2^46), and
key 7, padding (1 bloque256, 2 reforzado); it is 103 bytes without
extensions, whatever L.
- The payload is the content padded with zeros to P = rule(L). Step 17
checks the length and the zeros, and Open writes only the first L bytes.
- INNER_ACCESS_AGE holds exactly 16 X25519 stanzas: 1 to 16 credentials,
and a dummy in each slot left, in a uniformly random order.
Writer rules (spec §62.1): EncryptOptions.Length is required and the
source must deliver exactly that many bytes; recipients that are not
canonical or of low order are rejected (agewrap.CheckX25519Recipient);
self-checks of the header, the control, INNER_ACCESS_AGE and PAYLOAD_AGE.
The CLI measures its input, takes -padding and reports the format.
Test data: seven format 2 fixtures, padding vectors checked against
math/big, format 2 CBOR vectors, and the mutation corpus in both formats
with the 22 cases of the third list of spec §64, built without randomness
by sealing the fixtures again with their known keys and nonces. The
format 1 fixtures are kept byte for byte and never regenerated; the
differential corpus keeps its 1825 cases and adds a block per format 2
fixture. The spec copy loses its "to be implemented" markers, and the
READMEs, CHANGELOG, traceability and testdata/README.md follow v0.9.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
dkcs , err := filepath . Glob ( filepath . Join ( fixtures , "*.dkc" ) )
if err != nil {
t . Fatal ( err )
}
if len ( names ) != len ( dkcs ) || len ( dkcs ) != 26 {
t . Fatalf ( "%d frozen inspect outputs, want one per official .dkc (%d, 26)" , len ( names ) , len ( dkcs ) )
}
t . Chdir ( fixtures )
for _ , path := range names {
name := strings . TrimSuffix ( filepath . Base ( path ) , ".inspect.json" )
t . Run ( name , func ( t * testing . T ) {
want , err := os . ReadFile ( name + ".inspect.json" )
if err != nil {
t . Fatal ( err )
}
stdout , _ , err := cli ( t , later , "inspect" , "-json" , "-in" , name + ".dkc" )
if err != nil {
t . Fatal ( err )
}
if stdout != string ( want ) {
t . Fatalf ( "output differs from %s.inspect.json:\n%s" , name , stdout )
}
} )
}
}
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
func TestResolveAndProfile ( t * testing . T ) {
stdout , _ , err := cli ( t , later , "datekey" , "resolve" , "-at" , "2030-01-01T00:00:00Z" )
if err != nil || ! strings . Contains ( stdout , ` "round":66884212 ` ) || ! strings . Contains ( stdout , ` "unlock_at":"2030-01-01T00:00:00Z" ` ) {
t . Fatalf ( "%v %s" , err , stdout )
}
if _ , _ , err := cli ( t , later , "datekey" , "resolve" , "-at" , "2030-01-01 00:00" ) ; err == nil {
t . Fatal ( "accepted a time without zone" )
}
stdout , _ , err = cli ( t , later , "profile" , "hash" )
if err != nil || ! strings . Contains ( stdout , profile . QuicknetProfileHash ) || ! strings . Contains ( stdout , ` "pinned":true ` ) {
t . Fatalf ( "%v %s" , err , stdout )
}
b , _ := profile . Quicknet ( ) . CanonicalCBOR ( )
path := filepath . Join ( t . TempDir ( ) , "q.cbor" )
os . WriteFile ( path , b , 0 o600 )
if stdout , _ , err = cli ( t , later , "profile" , "hash" , "-in" , path ) ; err != nil || ! strings . Contains ( stdout , profile . QuicknetProfileHash ) {
t . Fatalf ( "%v %s" , err , stdout )
}
}
func TestUsage ( t * testing . T ) {
Version constants: the specification and the module
- datekeys.SpecVersion ("0.8.2") names the specification the module
implements. A test ties it to the spec file, its title and
spec/README.md, and TestCatalogueMatchesSpec and the vector files use
it (testkit.SpecVersion now aliases it), so the vectors regenerate
unchanged.
- datekeys.Version() is the version of the module as the go command
recorded it. That is a tag, or for a binary built in a checkout the
pseudo-version of its commit (for example
v0.0.0-20260928105528-9ac9cd952f04), or (devel) when it is unknown, as
in tests or under a replace directive to a directory. It works as the
main module and as a dependency, whatever the module path, which it
reads from the root package.
- `datekeys version` (also -version and --version) prints both and the
Go toolchain.
- README.md and README.es.md explain the three versions (format,
specification, module) and what the code on main covers.
traceability §70 and CHANGELOG follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
for _ , args := range [ ] [ ] string { nil , { "nope" } , { "datekey" } , { "profile" , "x" } , { "encrypt" , "-bogus" } , { "inspect" , "extra" } , { "version" , "extra" } } {
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
if _ , _ , err := cli ( t , later , args ... ) ; err == nil {
t . Errorf ( "%v accepted" , args )
}
}
}
Version constants: the specification and the module
- datekeys.SpecVersion ("0.8.2") names the specification the module
implements. A test ties it to the spec file, its title and
spec/README.md, and TestCatalogueMatchesSpec and the vector files use
it (testkit.SpecVersion now aliases it), so the vectors regenerate
unchanged.
- datekeys.Version() is the version of the module as the go command
recorded it. That is a tag, or for a binary built in a checkout the
pseudo-version of its commit (for example
v0.0.0-20260928105528-9ac9cd952f04), or (devel) when it is unknown, as
in tests or under a replace directive to a directory. It works as the
main module and as a dependency, whatever the module path, which it
reads from the root package.
- `datekeys version` (also -version and --version) prints both and the
Go toolchain.
- README.md and README.es.md explain the three versions (format,
specification, module) and what the code on main covers.
traceability §70 and CHANGELOG follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 week ago
// The version command names the module version, the specification and the
// toolchain; in a test the module version is unknown.
func TestVersion ( t * testing . T ) {
for _ , arg := range [ ] string { "version" , "-version" , "--version" } {
out , _ , err := cli ( t , later , arg )
if err != nil {
t . Fatal ( err )
}
want := "datekeys " + datekeys . Version ( ) + "\nspecification " + datekeys . SpecVersion + "\n" + runtime . Version ( ) + " " + runtime . GOOS + "/" + runtime . GOARCH + "\n"
if out != want {
t . Errorf ( "%s: %q, want %q" , arg , out , want )
}
}
if ! strings . Contains ( usage , "datekeys version" ) {
t . Error ( "the usage does not list the version command" )
}
}
Initial implementation of the DateKeys Protocol v0.8.1
Reference implementation in Go, built from the implementation plan
(milestones M0 to M5): datekey, profile, provider, codec, agewrap,
extension, capsule, accesskey, the datekeys CLI, official vectors and
fixtures, the mutation corpus, fuzz targets, interop and live tests,
CI workflows, traceability and policy documents.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 weeks ago
// Spec §53: long horizons get the harvest-now, decrypt-later warning.
func TestLongHorizonWarning ( t * testing . T ) {
dir := t . TempDir ( )
in := filepath . Join ( dir , "in" )
os . WriteFile ( in , [ ] byte ( "x" ) , 0 o600 )
for i , tc := range [ ] struct {
after time . Duration
warn bool
} { { time . Hour , false } , { 2 * 365 * 24 * time . Hour , true } } {
out := filepath . Join ( dir , fmt . Sprintf ( "%d.dkc" , i ) )
_ , stderr , err := cli ( t , later , "encrypt" , "-at" , later . Add ( tc . after ) . Format ( time . RFC3339 ) , "-in" , in , "-out" , out )
if err != nil || strings . Contains ( stderr , "not post-quantum" ) != tc . warn {
t . Fatalf ( "%s: %v: %s" , tc . after , err , stderr )
}
}
}
A key of words for the CLI: encrypt and decrypt with -words
The author asked for keys that people can keep without files. Package
wordkey derives the X25519 identity of words a person chooses, at least
six: their NFD by the tables of pathrule without the marks U+0300 to
U+036F, each code point in lower case by its simple mapping, split at
white space, joined by one space, and stretched with PBKDF2-HMAC-SHA256
of the standard library, 600 000 rounds, salted with the chain hash and
the round of the capsule. It is wordkey.ts of datekeys-ts byte for byte:
both check the same vector.
encrypt takes -words or -words-file for a time_and_key capsule, and adds
the key as one more recipient, derived for the round of -at; decrypt
takes them and adds the identity, for the round the capsule shows. The
format does not change. Checked: the CLI opened a capsule that the page
wrote with words, with the release from the public relays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 days ago
func TestKeyOfWords ( t * testing . T ) {
dir := t . TempDir ( )
in := filepath . Join ( dir , "carta.txt" )
os . WriteFile ( in , [ ] byte ( "abierta con palabras" ) , 0 o600 )
p := profile . Quicknet ( )
unlock := time . Unix ( p . GenesisTime + 999 * 3 , 0 ) . UTC ( ) // round 1000
genesis := time . Unix ( p . GenesisTime , 0 )
at := unlock . Format ( time . RFC3339 )
dkc := filepath . Join ( dir , "carta.dkc" )
if _ , stderr , err := cli ( t , genesis , "encrypt" , "-at" , at , "-policy" , "time_and_key" , "-words" , "Perro luna casa verde trén mar" , "-in" , in , "-out" , dkc ) ; err != nil {
t . Fatalf ( "encrypt: %v\n%s" , err , stderr )
}
words := filepath . Join ( dir , "palabras.txt" )
os . WriteFile ( words , [ ] byte ( " perro LUNA casa\nverde tren mar\n" ) , 0 o600 )
out := filepath . Join ( dir , "abierta" )
if _ , stderr , err := cli ( t , later , "decrypt" , "-in" , dkc , "-out" , out , "-words-file" , words , "-relay" , relay ( t ) ) ; err != nil {
t . Fatalf ( "decrypt: %v\n%s" , err , stderr )
}
if b , err := os . ReadFile ( filepath . Join ( out , "carta.txt" ) ) ; err != nil || string ( b ) != "abierta con palabras" {
t . Fatalf ( "carta.txt = %q, %v" , b , err )
}
if _ , _ , err := cli ( t , later , "decrypt" , "-in" , dkc , "-out" , filepath . Join ( dir , "otra" ) , "-words" , "gato luna casa verde tren mar" , "-relay" , relay ( t ) ) ; err == nil {
t . Fatal ( "other words opened the capsule" )
}
for _ , tc := range [ ] struct {
args [ ] string
want string
} {
{ [ ] string { "-policy" , "time_and_key" , "-words" , "uno dos tres" } , "at least 6 different words of 3 or more letters, not 3" } ,
{ [ ] string { "-policy" , "time_and_key" , "-words" , "de la casa al mar en tren verde" } , "not 4" } ,
{ [ ] string { "-policy" , "time_and_key" , "-words" , "perro luna casa verde tren mar" + string ( rune ( 0x200B ) ) } , "invisible character U+200B" } ,
A key of words for the CLI: encrypt and decrypt with -words
The author asked for keys that people can keep without files. Package
wordkey derives the X25519 identity of words a person chooses, at least
six: their NFD by the tables of pathrule without the marks U+0300 to
U+036F, each code point in lower case by its simple mapping, split at
white space, joined by one space, and stretched with PBKDF2-HMAC-SHA256
of the standard library, 600 000 rounds, salted with the chain hash and
the round of the capsule. It is wordkey.ts of datekeys-ts byte for byte:
both check the same vector.
encrypt takes -words or -words-file for a time_and_key capsule, and adds
the key as one more recipient, derived for the round of -at; decrypt
takes them and adds the identity, for the round the capsule shows. The
format does not change. Checked: the CLI opened a capsule that the page
wrote with words, with the release from the public relays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7 days ago
{ [ ] string { "-words" , "uno dos tres cuatro cinco seis" } , "need -policy time_and_key" } ,
{ [ ] string { "-policy" , "time_and_key" , "-words" , "a" , "-words-file" , words } , "are exclusive" } ,
} {
args := append ( [ ] string { "encrypt" , "-at" , at , "-in" , in , "-out" , filepath . Join ( dir , "x.dkc" ) } , tc . args ... )
if _ , _ , err := cli ( t , genesis , args ... ) ; err == nil || ! strings . Contains ( err . Error ( ) , tc . want ) {
t . Errorf ( "%v: %v, want %q" , tc . args , err , tc . want )
}
}
}
// Spec v0.11 §24.1: decrypt does not show a public note that breaks the rules
// of text, and says so.
func TestPresentUnusableNote ( t * testing . T ) {
tab := capsule . Header { Noncritical : [ ] extension . Extension { { ID : extension . NoteID , Version : 1 , Data : [ ] byte ( "a\tb" ) } } }
o := & capsule . Opened {
Verdicts : capsule . Verdicts { Signature : capsule . VerdictNoSignature , Seal : capsule . VerdictNoSeal } ,
Head : & capsule . Head { Files : [ ] capsule . File { { Path : "nota.txt" } } } ,
Inspection : & capsule . Inspection { Header : & tab } ,
}
var b bytes . Buffer
present ( & b , o , "DIR" , 80 )
if ! strings . Contains ( joined ( b . String ( ) ) , unusableNote + "\n" ) || strings . Contains ( b . String ( ) , noteTitle ) {
t . Errorf ( "an unusable note:\n%s" , b . String ( ) )
}
}