You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/cmd/datekeys/main.go

805 lines
30 KiB

// Command datekeys encrypts, inspects and opens DateKeyCap (.dkc) files.
//
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
// datekeys encrypt -at 2030-01-01T00:00:00Z -in fotos -in carta.txt -comment "Para Ana" -out regalo.dkc
// datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -dkk key.dkk -in secret.txt -out secret.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
// datekeys inspect -in regalo.dkc
// datekeys encrypt -at 2030-01-01T00:00:00Z -policy time_and_key -words-file palabras.txt -in carta.txt -out carta.dkc
// datekeys author keygen -out autor.key -pass-file clave.txt
// datekeys encrypt -at 2030-01-01T00:00:00Z -in carta.txt -sign autor.key -sign-pass-file clave.txt -out carta.dkc
// datekeys decrypt -in regalo.dkc -out regalo [-dkk key.dkk] [-identity key.txt] [-words-file palabras.txt]
// datekeys decrypt -in regalo.dkc -out regalo -release ronda.cbor
// datekeys datekey resolve -at 2030-01-01T00:00:00Z
// datekeys profile hash
// datekeys version
//
// Encryption never touches the network. Decryption fetches the release from
Release object, release in hand and step 9.c option B (spec v0.15 draft) The release of a round becomes a file, .dkr: a release object in deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}, which provider.EncodeRelease writes and DecodeRelease reads with its layers (size, type and version, schema). provider.ParseRelease also reads drand's JSON as the input of the caller. Verify checks the chain hash a release names before its round and its signature, with ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the informative format of the draft. capsule.OpenOptions.Release takes a release in hand, a provider.Supplier, exclusive with Source: Open does not compare it with the clock (step 9.c, option B) and reports a clock behind it in Opened.ClockBehind; a network source is still never asked before the round time. The CLI gains decrypt -release FILE (.dkr, drand's JSON or a local archive), decrypt -save-release FILE.dkr and the command release, which fetches, verifies and saves the .dkr without opening the capsule. Test data: vectors/release.json, releases/<round>.dkr for rounds 1000, 1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In mutations.json every case says its source, "supplied" or "network"; the case "round not reached yet", a release in hand, now opens, and four cases are added: the same with a network source, a release of another round from a network source, and two release objects of another chain. SpecVersion stays 0.14 until the author approves the draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
// public drand relays, or reads the one the person has in hand with -release,
// and verifies it locally. Outputs are written to a
// temporary file in the destination directory and published only when
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
// complete; existing files are never overwritten. The files of a format 3
// capsule go to a new folder, staged inside it and moved into place only
// when every check has passed.
package main
import (
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
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"filippo.io/age"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/accesskey"
"g.activething.com/go/DateKeys/authorkey"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/datekey"
"g.activething.com/go/DateKeys/internal/inspectview"
"g.activething.com/go/DateKeys/profile"
Release object, release in hand and step 9.c option B (spec v0.15 draft) The release of a round becomes a file, .dkr: a release object in deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}, which provider.EncodeRelease writes and DecodeRelease reads with its layers (size, type and version, schema). provider.ParseRelease also reads drand's JSON as the input of the caller. Verify checks the chain hash a release names before its round and its signature, with ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the informative format of the draft. capsule.OpenOptions.Release takes a release in hand, a provider.Supplier, exclusive with Source: Open does not compare it with the clock (step 9.c, option B) and reports a clock behind it in Opened.ClockBehind; a network source is still never asked before the round time. The CLI gains decrypt -release FILE (.dkr, drand's JSON or a local archive), decrypt -save-release FILE.dkr and the command release, which fetches, verifies and saves the .dkr without opening the capsule. Test data: vectors/release.json, releases/<round>.dkr for rounds 1000, 1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In mutations.json every case says its source, "supplied" or "network"; the case "round not reached yet", a release in hand, now opens, and four cases are added: the same with a network source, a release of another round from a network source, and two release objects of another chain. SpecVersion stays 0.14 until the author approves the draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
"g.activething.com/go/DateKeys/provider"
"g.activething.com/go/DateKeys/provider/drand"
"g.activething.com/go/DateKeys/wordkey"
)
const usage = `usage:
datekeys encrypt -at TIME -in FILE|FOLDER... -out FILE.dkc [-comment TEXT] [-author TEXT] [-no-mtime] [-policy time_only|time_and_key] [-recipient age1...]... [-dkk FILE.dkk] [-words TEXT|-words-file FILE|-new-words FILE [-dic LIST] [-word-count N]|-dice TEXT|-dice-file FILE [-dic LIST]] [-padding reforzado|bloque256] [-note TEXT] [-sign KEY [-sign-pass-file FILE]] [-large-area] [-no-recovery]
datekeys decrypt -in FILE.dkc -out PATH [-dkk FILE.dkk] [-identity FILE]... [-words TEXT|-words-file FILE] [-expect-author dkauthor1...] [-relay URL]... [-release FILE]
datekeys author keygen -out FILE (-pass-file FILE|-plain)
datekeys author public -key FILE [-pass-file FILE]
datekeys inspect -in FILE.dkc [-json]
datekeys datekey resolve -at TIME
datekeys profile hash [-in PROFILE.cbor]
datekeys wordlist [-dic LIST]
datekeys version
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
TIME is RFC 3339 with a time zone, for example 2030-01-01T00:00:00Z.
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
encrypt writes capsule format 3: the files of each -in, a folder by its name
and the files below it, with an optional comment and declared author. The
content is padded, by default with the rule reforzado, and a time_and_key
capsule holds 16 slots, from 1 to 16 credentials and a dummy in each slot
left (spec §29, §39). decrypt writes the files of a format 3 capsule to the
new folder PATH, and the content of formats 1 and 2 to the new file PATH.
Release object, release in hand and step 9.c option B (spec v0.15 draft) The release of a round becomes a file, .dkr: a release object in deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}, which provider.EncodeRelease writes and DecodeRelease reads with its layers (size, type and version, schema). provider.ParseRelease also reads drand's JSON as the input of the caller. Verify checks the chain hash a release names before its round and its signature, with ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the informative format of the draft. capsule.OpenOptions.Release takes a release in hand, a provider.Supplier, exclusive with Source: Open does not compare it with the clock (step 9.c, option B) and reports a clock behind it in Opened.ClockBehind; a network source is still never asked before the round time. The CLI gains decrypt -release FILE (.dkr, drand's JSON or a local archive), decrypt -save-release FILE.dkr and the command release, which fetches, verifies and saves the .dkr without opening the capsule. Test data: vectors/release.json, releases/<round>.dkr for rounds 1000, 1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In mutations.json every case says its source, "supplied" or "network"; the case "round not reached yet", a release in hand, now opens, and four cases are added: the same with a network source, a release of another round from a network source, and two release objects of another chain. SpecVersion stays 0.14 until the author approves the draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
decrypt fetches the release of the round from drand relays, never before the
round time. -release FILE gives it instead, without any network request: a
release object, drand's JSON or a local release archive. It is verified like
one from a relay, against the pinned key, so it does not matter who served
it; and the clock does not stop it: a valid release proves that the round
was published (spec v0.15, §63 step 9.c).
Release object, release in hand and step 9.c option B (spec v0.15 draft) The release of a round becomes a file, .dkr: a release object in deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}, which provider.EncodeRelease writes and DecodeRelease reads with its layers (size, type and version, schema). provider.ParseRelease also reads drand's JSON as the input of the caller. Verify checks the chain hash a release names before its round and its signature, with ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the informative format of the draft. capsule.OpenOptions.Release takes a release in hand, a provider.Supplier, exclusive with Source: Open does not compare it with the clock (step 9.c, option B) and reports a clock behind it in Opened.ClockBehind; a network source is still never asked before the round time. The CLI gains decrypt -release FILE (.dkr, drand's JSON or a local archive), decrypt -save-release FILE.dkr and the command release, which fetches, verifies and saves the .dkr without opening the capsule. Test data: vectors/release.json, releases/<round>.dkr for rounds 1000, 1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In mutations.json every case says its source, "supplied" or "network"; the case "round not reached yet", a release in hand, now opens, and four cases are added: the same with a network source, a release of another round from a network source, and two release objects of another chain. SpecVersion stays 0.14 until the author approves the draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
-words and -words-file give a key of words to a time_and_key capsule: at
least 6 different words of 3 or more letters that open it with decrypt,
instead of a .dkk
(wordkey). Case, accents and extra spaces do not matter. -words leaves them
in the shell history; -words-file reads them from a file. Never use a
password used anywhere else: once the date has come, the capsule lets
whoever holds it test guesses of it.
-new-words FILE draws the words at random instead, and writes them to the new
file FILE: -word-count words, 7 by default, of the list -dic of 7776 words,
en by default, the list of the EFF, or es. Words a person chooses are weaker
than random ones: whoever holds the .dkc can try them offline once the date
has come (spec §38.1).
-dice TEXT and -dice-file FILE take the words from dice instead, for whoever
does not trust the random numbers of a computer: five dice for each word,
read in a fixed order, give a number from 11111 to 66666, the position of a
word in the list -dic, at least 6 numbers and no word twice. datekeys
wordlist writes the list -dic numbered for dice, to print it, and its
SHA-256: for en it is the list of the EFF, byte for byte. encrypt shows the
words: keep the words, which open the capsule; the numbers give them only
with that list.
encrypt also writes FILE.dkc.recuperacion.txt next to the capsule, unless
-no-recovery: the annex of the specification, in Spanish, on how to open a
capsule without DateKeys software (spec §79), the same for every capsule.
Opening it years later needs the .dkc, a credential of a time_and_key
capsule, and the release of its round, which drand publishes at the date:
if drand no longer serves it then, an archive of releases or a cache service
must have kept it (spec §50). For a long horizon or a valuable content,
time_and_key adds a credential that drand does not hold (spec §7.6). A
profile that is not active writes no capsule, and decrypt and inspect warn
when the profile of a capsule is compromised (spec §71).
-note puts a public note in the capsule, in clear: anyone who has the .dkc
reads it before the date, nobody can check who wrote it, and with the date it
can identify someone. decrypt shows it as text of the creator.
-sign signs the capsule with the author key in the file KEY, made by author
keygen (spec v0.11, §29.9). A key encrypted with a passphrase needs
-sign-pass-file: a file with the passphrase, or - for the standard input. The
passphrase is never taken from the command line or the environment. A
signature proves that whoever has the secret key signed, not who that is.
-large-area lets the security area grow from 32 KiB to 64 KiB if a signature does not fit. decrypt always
shows the signature; with -expect-author it fails, and writes no file, unless
the capsule is signed with that public key.`
// errUsage reports a malformed command line; main prints the usage text.
var stdin io.Reader = os.Stdin
var errUsage = errors.New("invalid command line; run 'datekeys help'")
// longHorizon is the product policy threshold for the harvest-now,
// decrypt-later warning (spec §53).
const longHorizon = 365 * 24 * time.Hour
// profileStatus is the state of a profile in the registry of profiles
// (spec §71), as this release of the module knows it; the tests replace it
// to see a profile that is not active.
var profileStatus = func(p *profile.Profile) profile.Status {
h, err := p.Hash()
if err != nil {
return profile.Active
}
s, _ := profile.StatusOf(h)
return s
}
func main() {
if err := run(os.Args[1:], os.Stdout, os.Stderr, time.Now); err != nil {
if errors.Is(err, errUsage) {
fmt.Fprintln(os.Stderr, usage)
os.Exit(2)
}
fmt.Fprintln(os.Stderr, "datekeys:", err)
if code := datekeys.Code(err); code != "" {
fmt.Fprintln(os.Stderr, "datekeys: error code", code)
}
os.Exit(1)
}
}
type multi []string
func (m *multi) String() string { return strings.Join(*m, ",") }
func (m *multi) Set(v string) error { *m = append(*m, v); return nil }
// run is the CLI; the clock is injected for tests (only the CLI reads the
// wall clock).
func run(args []string, stdout, stderr io.Writer, now func() time.Time) error {
if len(args) == 0 {
return errUsage
}
switch args[0] {
case "encrypt":
return encrypt(args[1:], stderr, now)
case "decrypt":
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
return decrypt(args[1:], stdout, stderr, now)
case "author":
return author(args[1:], stdout, stderr, stdin)
case "inspect":
return inspect(args[1:], stdout)
case "datekey":
if len(args) < 2 || args[1] != "resolve" {
return errUsage
}
return resolve(args[2:], stdout)
case "profile":
if len(args) < 2 || args[1] != "hash" {
return errUsage
}
return profileHash(args[2:], stdout)
case "wordlist":
return wordList(args[1:], stdout, stderr)
case "version", "-version", "--version":
if len(args) != 1 {
return errUsage
}
// The module version (a tag, or the pseudo-version of the commit a
// checkout was built from), the specification it implements and the
// toolchain.
fmt.Fprintf(stdout, "datekeys %s\nspecification %s\n%s %s/%s\n", datekeys.Version(), datekeys.SpecVersion, runtime.Version(), runtime.GOOS, runtime.GOARCH)
return nil
case "-h", "-help", "--help", "help":
fmt.Fprintln(stdout, usage)
return nil
}
return errUsage
}
func newFlags(name string) *flag.FlagSet {
fs := flag.NewFlagSet(name, flag.ContinueOnError)
fs.SetOutput(io.Discard)
return fs
}
func parse(fs *flag.FlagSet, args []string) error {
if err := fs.Parse(args); err != nil {
return fmt.Errorf("%s: %w", fs.Name(), err)
}
if fs.NArg() != 0 {
return fmt.Errorf("%s: unexpected arguments %q", fs.Name(), fs.Args())
}
return nil
}
func parseTime(s string) (time.Time, error) {
t, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
return time.Time{}, fmt.Errorf("invalid -at %q: RFC 3339 with a time zone is required", s)
}
return t, nil
}
func encrypt(args []string, stderr io.Writer, now func() time.Time) error {
fs := newFlags("encrypt")
at := fs.String("at", "", "unlock time, RFC 3339")
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
var ins multi
fs.Var(&ins, "in", "file or folder to encrypt (repeatable)")
out := fs.String("out", "", "new .dkc file; never overwritten")
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
comment := fs.String("comment", "", "comment for whoever opens the capsule, shown as text of the creator")
author := fs.String("author", "", "declared author, shown as text of the creator that proves nothing")
noMTime := fs.Bool("no-mtime", false, "leave out the modification times of the files")
policy := fs.String("policy", "time_only", "time_only or time_and_key")
dkk := fs.String("dkk", "", "time_and_key: new .dkk file for a portable access key")
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
padding := fs.String("padding", "reforzado", "padding rule of the content: reforzado or bloque256")
var recipients multi
fs.Var(&recipients, "recipient", "time_and_key: X25519 recipient age1... (repeatable)")
words := fs.String("words", "", "time_and_key: at least 6 words that open the capsule; they stay in the shell history")
wordsFile := fs.String("words-file", "", "time_and_key: file with the words that open the capsule")
newWords := fs.String("new-words", "", "time_and_key: new file with words drawn at random from a list, that open the capsule")
dice := fs.String("dice", "", "time_and_key: numbers of five dice, one for each word of the list -dic; they stay in the shell history")
diceFile := fs.String("dice-file", "", "time_and_key: file with the numbers of five dice of -dice")
dic := fs.String("dic", "en", "list of -new-words and -dice: "+strings.Join(wordkey.Languages(), ", "))
wordCount := fs.Int("word-count", wordkey.DefaultCount, "number of words of -new-words, 6 or more")
note := fs.String("note", "", "public note of the capsule: one line that anyone with the .dkc reads before the date, and that can identify someone with it")
sign := fs.String("sign", "", "file with the author key that signs the capsule")
signPass := fs.String("sign-pass-file", "", "file with the passphrase of the author key, or - for the standard input")
noRecovery := fs.Bool("no-recovery", false, "do not write the recovery annex of the specification next to the .dkc (spec §79)")
largeArea := fs.Bool("large-area", false, "let the security area grow to 64 KiB if a signature does not fit in 32 KiB (an author key always fits)")
if err := parse(fs, args); err != nil {
return err
}
unlock, err := parseTime(*at)
if err != nil {
return err
}
pol, err := capsule.ParsePolicy(*policy)
if err != nil {
return 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
var code capsule.Padding
switch *padding {
case capsule.Reforzado.String():
code = capsule.Reforzado
case capsule.Bloque256.String():
code = capsule.Bloque256
default:
return fmt.Errorf("encrypt: unknown padding rule %q: reforzado or bloque256", *padding)
}
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 *out == "" || len(ins) == 0 && *comment == "" {
return errors.New("encrypt: -out, and -in or -comment, are required")
}
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
opts := capsule.EncryptOptions{Profile: profile.Quicknet(), UnlockAt: unlock, Policy: pol, NewPortableKey: *dkk != "", Padding: code,
Comment: *comment, Author: *author, Now: now}
if s := profileStatus(opts.Profile); s != profile.Active {
return fmt.Errorf("encrypt: the profile %s is %s: no new capsule is written with it (spec §71)", opts.Profile.ID, s)
}
for _, r := range recipients {
x, err := age.ParseX25519Recipient(r)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
opts.Recipients = append(opts.Recipients, x)
}
// The key of words is one more credential: the writer derives it once it
// has drawn capsule_id, which salts it (spec §38.1).
text, err := wordsText("encrypt", *words, *wordsFile)
if err != nil {
return err
}
diceText, err := flagText("encrypt", "dice", *dice, *diceFile)
if err != nil {
return err
}
if diceText != "" && (text != "" || *newWords != "") {
return errors.New("encrypt: -dice and -dice-file exclude -words, -words-file and -new-words")
}
var listSize int
var fromDice []string
if *newWords != "" {
if text != "" {
return errors.New("encrypt: -new-words excludes -words and -words-file")
}
if err := checkNew(*newWords); err != nil {
return err
}
list, err := wordkey.List(*dic)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
drawn, err := wordkey.Generate(list, *wordCount, nil)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
text = strings.Join(drawn, " ")
listSize = len(list)
}
if diceText != "" {
list, err := wordkey.List(*dic)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
if fromDice, err = wordkey.DiceWords(list, diceText); err != nil {
return fmt.Errorf("encrypt: %w", err)
}
text = strings.Join(fromDice, " ")
listSize = len(list)
}
if text != "" {
if pol != capsule.TimeAndKey {
return errors.New("encrypt: -words, -words-file, -new-words and -dice need -policy time_and_key")
}
w := wordkey.Normalize(text)
if err := wordkey.Check(w); err != nil {
return fmt.Errorf("encrypt: %w", err)
}
opts.Words = w
}
if *dkk != "" {
if err := checkNew(*dkk); err != nil {
return err
}
}
annex := *out + datekeys.RecoveryAnnexSuffix
if !*noRecovery {
if err := checkNew(annex); err != nil {
return err
}
}
if *signPass != "" && *sign == "" {
return errors.New("encrypt: -sign-pass-file needs -sign")
}
if *sign != "" {
k, err := loadAuthorKey("encrypt", *sign, *signPass, stdin)
if err != nil {
return err
}
defer k.Clear()
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
opts.AuthorKey = announced{k, stderr}
}
opts.LargeArea = *largeArea
opts.PublicNote = *note
if *note != "" {
fmt.Fprintln(stderr, "warning: the public note is in clear: anyone who has the .dkc reads it before the date, nobody can delete it from the copies that circulate, and with the date it can identify someone (spec §24.1).")
}
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
sources, skipped, err := collect(ins, !*noMTime)
if err != nil {
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
return fmt.Errorf("encrypt: %w", err)
}
var res *capsule.Result
err = writeAtomic(*out, func(w io.Writer) error {
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
res, err = capsule.EncryptFiles(w, sources, opts)
return err
})
if err != nil {
return err
}
if res.PortableKey != nil {
defer res.PortableKey.Wipe()
if err := writeAtomic(*dkk, func(w io.Writer) error { return accesskey.Encode(w, res.PortableKey) }); err != nil {
return fmt.Errorf("the capsule was written to %s but its .dkk could not be: %w", *out, err)
}
}
if *newWords != "" {
if err := writeAtomic(*newWords, func(w io.Writer) error { _, err := io.WriteString(w, text+"\n"); return err }); err != nil {
return fmt.Errorf("the capsule was written to %s but its words could not be: %w", *out, err)
}
}
if !*noRecovery {
if err := writeAtomic(annex, func(w io.Writer) error { _, err := io.WriteString(w, datekeys.RecoveryAnnex); return err }); err != nil {
return fmt.Errorf("the capsule was written to %s but its recovery annex could not be: %w", *out, err)
}
}
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
fmt.Fprintf(stderr, "Encrypted locally for %s (round %d)\n datekey %s\n capsule_id %x\n format %d: %d files, a payload of %d bytes, padded to %d (%s)\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
res.UnlockAt.Format(time.RFC3339), res.DateKey.Round, res.DateKey.Compact(), res.CapsuleID, res.Format, len(res.Head.Files), res.Length, res.PaddedLength, res.Padding)
for _, p := range skipped {
fmt.Fprintf(stderr, " left out %s, which the system creates on its own\n", p)
}
if res.PortableKey != nil {
fmt.Fprintf(stderr, " access key %s: keep it secret; it is valid for this capsule only\n", *dkk)
}
if *newWords != "" {
fmt.Fprintf(stderr, " words %s: %d words of the list %s, %d bits; keep them secret, or write them down and delete the file\n",
*newWords, *wordCount, *dic, int(wordkey.Bits(listSize, *wordCount)))
}
if fromDice != nil {
fmt.Fprintf(stderr, " words %s: the %d words of the dice in the list %s, %d bits; keep these words, which open the capsule: the numbers give them only with this list\n",
text, len(fromDice), *dic, int(wordkey.Bits(listSize, len(fromDice))))
}
if !*noRecovery {
fmt.Fprintf(stderr, " recovery %s: how to open the capsule without DateKeys software (spec §79); keep it with the .dkc\n", annex)
}
// What opening it years later will take (spec §62.1, rule 26).
needs := "the .dkc"
if pol == capsule.TimeAndKey {
needs = "the .dkc, one of its credentials"
}
fmt.Fprintf(stderr, " to open %s and the release of round %d, which drand publishes at %s: years later, if drand no longer\n"+
" serves it, an archive of releases or a cache service must have kept it (spec §50)\n",
needs, res.DateKey.Round, res.UnlockAt.Format(time.RFC3339))
if res.UnlockAt.Sub(now()) > longHorizon {
fmt.Fprintln(stderr, "warning: Quicknet V1 timelock is not post-quantum. The ciphertext may stay available for years,\n"+
" and its future confidentiality depends on the provider and on the underlying cryptography (spec §53).")
if pol == capsule.TimeOnly {
fmt.Fprintln(stderr, "note: for a horizon this long, or a valuable content, -policy time_and_key adds a credential that drand does\n"+
" not hold: an early signature of the round would not open the capsule (spec §7.6).")
}
}
return nil
}
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 decrypt(args []string, stdout, stderr io.Writer, now func() time.Time) error {
fs := newFlags("decrypt")
in := fs.String("in", "", ".dkc file")
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
out := fs.String("out", "", "new folder (format 3) or file (formats 1 and 2); never overwritten")
dkk := fs.String("dkk", "", "portable access key (.dkk)")
timeout := fs.Duration("timeout", 30*time.Second, "release request timeout")
var identities, relays multi
fs.Var(&identities, "identity", "age identity file with X25519 keys (repeatable)")
fs.Var(&relays, "relay", "drand relay base URL (repeatable); default: public relays")
words := fs.String("words", "", "the words of a key of words; they stay in the shell history")
wordsFile := fs.String("words-file", "", "file with the words of a key of words")
expect := fs.String("expect-author", "", "fail unless the capsule is signed with this public key, dkauthor1...")
release := fs.String("release", "", "the release in hand: a release object, drand's JSON or a local release archive; no network request")
if err := parse(fs, args); err != nil {
return err
}
if *in == "" || *out == "" {
return errors.New("decrypt: -in and -out are required")
}
Release object, release in hand and step 9.c option B (spec v0.15 draft) The release of a round becomes a file, .dkr: a release object in deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}, which provider.EncodeRelease writes and DecodeRelease reads with its layers (size, type and version, schema). provider.ParseRelease also reads drand's JSON as the input of the caller. Verify checks the chain hash a release names before its round and its signature, with ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the informative format of the draft. capsule.OpenOptions.Release takes a release in hand, a provider.Supplier, exclusive with Source: Open does not compare it with the clock (step 9.c, option B) and reports a clock behind it in Opened.ClockBehind; a network source is still never asked before the round time. The CLI gains decrypt -release FILE (.dkr, drand's JSON or a local archive), decrypt -save-release FILE.dkr and the command release, which fetches, verifies and saves the .dkr without opening the capsule. Test data: vectors/release.json, releases/<round>.dkr for rounds 1000, 1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In mutations.json every case says its source, "supplied" or "network"; the case "round not reached yet", a release in hand, now opens, and four cases are added: the same with a network source, a release of another round from a network source, and two release objects of another chain. SpecVersion stays 0.14 until the author approves the draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
if *release != "" && len(relays) > 0 {
return errors.New("decrypt: -release and -relay are exclusive")
}
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
var expected []byte
if *expect != "" {
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
k, err := authorkey.ParsePublic(*expect)
if err != nil {
return fmt.Errorf("decrypt: -expect-author: %w", err)
}
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
expected = k
}
reg, err := profile.Default()
if err != nil {
return err
}
Release object, release in hand and step 9.c option B (spec v0.15 draft) The release of a round becomes a file, .dkr: a release object in deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}, which provider.EncodeRelease writes and DecodeRelease reads with its layers (size, type and version, schema). provider.ParseRelease also reads drand's JSON as the input of the caller. Verify checks the chain hash a release names before its round and its signature, with ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the informative format of the draft. capsule.OpenOptions.Release takes a release in hand, a provider.Supplier, exclusive with Source: Open does not compare it with the clock (step 9.c, option B) and reports a clock behind it in Opened.ClockBehind; a network source is still never asked before the round time. The CLI gains decrypt -release FILE (.dkr, drand's JSON or a local archive), decrypt -save-release FILE.dkr and the command release, which fetches, verifies and saves the .dkr without opening the capsule. Test data: vectors/release.json, releases/<round>.dkr for rounds 1000, 1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In mutations.json every case says its source, "supplied" or "network"; the case "round not reached yet", a release in hand, now opens, and four cases are added: the same with a network source, a release of another round from a network source, and two release objects of another chain. SpecVersion stays 0.14 until the author approves the draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
opts := capsule.OpenOptions{Registry: reg, Now: now}
if *release != "" {
supplier, closer, err := releaseInHand(*release)
if err != nil {
return err
}
defer closer.Close()
opts.Release = supplier
} else {
opts.Source = drand.New(relays...)
}
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 expected != nil {
// The expected key is not a key that the person saved, with a label
// she gave it: a signature with it is F4, which shows the whole key. A
// capsule that is not signed with it is refused before step 18, so
// that none of its files is ever written.
opts.Accept = func(v capsule.Verdicts) error {
if v.Signature == capsule.VerdictSignedOther && bytes.Equal(v.AuthorKey[:], expected) {
return nil
}
writeVerdicts(stdout, v.Lines(), outputWidth(stdout))
return fmt.Errorf("decrypt: the capsule is not signed with the expected key %s: nothing was written to %s", *expect, *out)
}
}
for _, path := range identities {
ids, err := readIdentities(path)
if err != nil {
return err
}
opts.Identities = append(opts.Identities, ids...)
}
if *dkk != "" {
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
// Open decodes the .dkk at step 9.a, and only for a time_and_key
// capsule, so that its errors come in the order of spec §63.
f, err := os.Open(*dkk)
if err != nil {
return 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
defer f.Close()
opts.AccessKeyFile = f
}
src, err := os.Open(*in)
if err != nil {
return err
}
defer src.Close()
text, err := wordsText("decrypt", *words, *wordsFile)
if err != nil {
return err
}
if text != "" {
// The words are salted with the chain and the round of the capsule:
// steps 1 to 8 give them. When they fail, Open reports why.
if insp, err := capsule.Inspect(src, capsule.InspectOptions{Registry: reg}); err == nil {
id, err := wordkey.Identity(wordkey.Normalize(text), insp.Profile.ChainHash[:], insp.Header.DateKey.Round, insp.Header.CapsuleID[:])
if err != nil {
return fmt.Errorf("decrypt: %w", err)
}
opts.Identities = append(opts.Identities, id)
}
if _, err := src.Seek(0, io.SeekStart); err != nil {
return err
}
}
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
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 format decides the output: a new folder for the files of format 3,
// a new file for the content of formats 1 and 2. A prelude that does not
// parse goes the second way, and Open reports it at step 1 or 2.
var pre [capsule.PreludeSize]byte
n, _ := src.ReadAt(pre[:], 0)
var opened *capsule.Opened
p, perr := capsule.ParsePrelude(pre[:n])
if *expect != "" && perr == nil && p.Format != capsule.Format3 {
// Only a capsule of format 3 has an author signature: fail before
// the release is requested and before anything is written. A prelude
// that does not parse is left to Open, which reports it at step 1 or
// 2 with its code.
return errors.New("decrypt: -expect-author: only a capsule of format 3 has an author signature, and this is not one")
}
if perr == nil && p.Format == capsule.Format3 {
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 := checkNew(*out); err != nil {
return err
}
// The folder is created at step 17, after the release is requested:
// its parent must be a folder before then.
if info, err := os.Stat(filepath.Dir(*out)); err != nil || !info.IsDir() {
return fmt.Errorf("decrypt: %s cannot be created: %s is not a folder", *out, filepath.Dir(*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
opts.Sink = &dirSink{dir: *out}
if opened, err = capsule.Open(ctx, nil, src, opts); err != nil {
return err
}
} else {
err = writeAtomic(*out, func(w io.Writer) error {
opened, err = capsule.Open(ctx, w, src, opts)
return err
})
if err != nil {
return 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
fmt.Fprintf(stderr, "Decrypted capsule %s (round %d, unlocked at %s); release verified locally\n",
opened.Inspection.Header.CapsuleIDHex(), opened.Release.Round, opened.Inspection.UnlockAt.Format(time.RFC3339))
if profileStatus(opened.Inspection.Profile) == profile.Compromised {
fmt.Fprintf(stderr, "warning: the profile %s is compromised: the content of this capsule may have been read before its date (spec §71)\n",
opened.Inspection.Profile.ID)
}
Release object, release in hand and step 9.c option B (spec v0.15 draft) The release of a round becomes a file, .dkr: a release object in deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}, which provider.EncodeRelease writes and DecodeRelease reads with its layers (size, type and version, schema). provider.ParseRelease also reads drand's JSON as the input of the caller. Verify checks the chain hash a release names before its round and its signature, with ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the informative format of the draft. capsule.OpenOptions.Release takes a release in hand, a provider.Supplier, exclusive with Source: Open does not compare it with the clock (step 9.c, option B) and reports a clock behind it in Opened.ClockBehind; a network source is still never asked before the round time. The CLI gains decrypt -release FILE (.dkr, drand's JSON or a local archive), decrypt -save-release FILE.dkr and the command release, which fetches, verifies and saves the .dkr without opening the capsule. Test data: vectors/release.json, releases/<round>.dkr for rounds 1000, 1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In mutations.json every case says its source, "supplied" or "network"; the case "round not reached yet", a release in hand, now opens, and four cases are added: the same with a network source, a release of another round from a network source, and two release objects of another chain. SpecVersion stays 0.14 until the author approves the draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
if opened.ClockBehind {
fmt.Fprintf(stderr, "warning: the release proves that round %d was published at %s, and this clock says %s: it may be behind\n",
opened.Release.Round, opened.Inspection.UnlockAt.Format(time.RFC3339), now().UTC().Format(time.RFC3339))
}
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 opened.Format == capsule.Format3 {
fmt.Fprintf(stderr, " format 3, %d files\n", len(opened.Head.Files))
if len(opened.Head.Files) == 0 {
fmt.Fprintf(stderr, " no files: %s was not created\n", *out)
}
present(stdout, opened, *out, outputWidth(stdout))
return nil
}
fmt.Fprintf(stderr, " format %d, %d bytes of content\n", opened.Format, opened.PayloadLength)
if *expect != "" {
return fmt.Errorf("decrypt: -expect-author: a capsule of format %d has no author signature", opened.Format)
}
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
if opened.Format == capsule.Format1 {
// Spec §55.2, §70: format 1 hides neither the number of credentials
// nor the exact length of the content.
fmt.Fprintln(stderr, " format 1 does not hide the number of credentials or the exact length of the content")
}
return nil
}
Release object, release in hand and step 9.c option B (spec v0.15 draft) The release of a round becomes a file, .dkr: a release object in deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}, which provider.EncodeRelease writes and DecodeRelease reads with its layers (size, type and version, schema). provider.ParseRelease also reads drand's JSON as the input of the caller. Verify checks the chain hash a release names before its round and its signature, with ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the informative format of the draft. capsule.OpenOptions.Release takes a release in hand, a provider.Supplier, exclusive with Source: Open does not compare it with the clock (step 9.c, option B) and reports a clock behind it in Opened.ClockBehind; a network source is still never asked before the round time. The CLI gains decrypt -release FILE (.dkr, drand's JSON or a local archive), decrypt -save-release FILE.dkr and the command release, which fetches, verifies and saves the .dkr without opening the capsule. Test data: vectors/release.json, releases/<round>.dkr for rounds 1000, 1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In mutations.json every case says its source, "supplied" or "network"; the case "round not reached yet", a release in hand, now opens, and four cases are added: the same with a network source, a release of another round from a network source, and two release objects of another chain. SpecVersion stays 0.14 until the author approves the draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
// releaseInHand reads the release that the person has in hand, from path: a
// local release archive, which is read when Open asks for the round, or a
// release object or drand's JSON, which Open decodes at step 10. A file larger
Release object, release in hand and step 9.c option B (spec v0.15 draft) The release of a round becomes a file, .dkr: a release object in deterministic CBOR, {0: "datekeys-release", 1: 1, 2: chain_hash, 3: round, 4: signature}, which provider.EncodeRelease writes and DecodeRelease reads with its layers (size, type and version, schema). provider.ParseRelease also reads drand's JSON as the input of the caller. Verify checks the chain hash a release names before its round and its signature, with ERR_PROFILE_MISMATCH. provider.Archive reads a local release archive, the informative format of the draft. capsule.OpenOptions.Release takes a release in hand, a provider.Supplier, exclusive with Source: Open does not compare it with the clock (step 9.c, option B) and reports a clock behind it in Opened.ClockBehind; a network source is still never asked before the round time. The CLI gains decrypt -release FILE (.dkr, drand's JSON or a local archive), decrypt -save-release FILE.dkr and the command release, which fetches, verifies and saves the .dkr without opening the capsule. Test data: vectors/release.json, releases/<round>.dkr for rounds 1000, 1001, 1004 and 2000, and a local archive of rounds 1000 to 1004. In mutations.json every case says its source, "supplied" or "network"; the case "round not reached yet", a release in hand, now opens, and four cases are added: the same with a network source, a release of another round from a network source, and two release objects of another chain. SpecVersion stays 0.14 until the author approves the draft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
// than any of them is cut where it can no longer be valid, so that step 10
// rejects it with the code of its size.
func releaseInHand(path string) (provider.Supplier, io.Closer, error) {
f, err := os.Open(path)
if err != nil {
return nil, nil, err
}
info, err := f.Stat()
if err != nil {
f.Close()
return nil, nil, err
}
head := make([]byte, min(info.Size(), 64))
if _, err := io.ReadFull(f, head); err != nil {
f.Close()
return nil, nil, err
}
if provider.IsArchive(head) {
return provider.NewArchive(f, info.Size()), f, nil
}
defer f.Close()
rest, err := io.ReadAll(io.LimitReader(f, provider.MaxReleaseJSONSize+1-int64(len(head))))
if err != nil {
return nil, nil, err
}
return provider.Encoded(append(head, rest...)), io.NopCloser(nil), nil
}
// wordsText is the text of the words of -words or of -words-file, at most
// 4 KiB, or "" when neither is given.
func wordsText(cmd, words, file string) (string, error) {
return flagText(cmd, "words", words, file)
}
// flagText is the text of -NAME or of the file of -NAME-file, at most 4 KiB,
// or "" when neither is given.
func flagText(cmd, name, text, file string) (string, error) {
if text != "" && file != "" {
return "", fmt.Errorf("%s: -%s and -%s-file are exclusive", cmd, name, name)
}
if file == "" {
return text, nil
}
f, err := os.Open(file)
if err != nil {
return "", err
}
defer f.Close()
b, err := io.ReadAll(io.LimitReader(f, 4<<10+1))
if err != nil {
return "", err
}
if len(b) > 4<<10 {
return "", fmt.Errorf("%s: %s is longer than 4 KiB: it is not a list of %s", cmd, file, name)
}
return string(b), nil
}
// wordList writes the list -dic numbered for dice to stdout, as the EFF
// publishes its list, to print it, and its SHA-256 to stderr, to compare it
// with the one that wordkey/lists/README.md records.
func wordList(args []string, stdout, stderr io.Writer) error {
fs := newFlags("wordlist")
dic := fs.String("dic", "en", "list: "+strings.Join(wordkey.Languages(), ", "))
if err := parse(fs, args); err != nil {
return err
}
list, err := wordkey.List(*dic)
if err != nil {
return fmt.Errorf("wordlist: %w", err)
}
text, err := wordkey.DiceList(list)
if err != nil {
return fmt.Errorf("wordlist: %w", err)
}
if _, err := io.WriteString(stdout, text); err != nil {
return err
}
fmt.Fprintf(stderr, "the list %s numbered for dice, %d words, SHA-256 %x\n", *dic, len(list), sha256.Sum256([]byte(text)))
return nil
}
func readIdentities(path string) ([]age.Identity, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
ids, err := age.ParseIdentities(f)
if err != nil {
return nil, fmt.Errorf("%s: %w", path, err)
}
return ids, nil
}
// inspect runs steps 1 to 8 only: it never requests a release and never uses
// a secret.
func inspect(args []string, stdout io.Writer) error {
fs := newFlags("inspect")
in := fs.String("in", "", ".dkc file")
asJSON := fs.Bool("json", false, "JSON output")
if err := parse(fs, args); err != nil {
return err
}
if *in == "" {
return errors.New("inspect: -in is required")
}
reg, err := profile.Default()
if err != nil {
return err
}
f, err := os.Open(*in)
if err != nil {
return err
}
defer f.Close()
result, inspectErr := capsule.Inspect(f, capsule.InspectOptions{Registry: reg})
v := inspectview.New(*in, result, inspectErr)
if *asJSON {
if err := v.WriteJSON(stdout); err != nil {
return err
}
} else {
v.WriteText(stdout)
showNote(stdout, result)
if result != nil && result.Profile != nil && profileStatus(result.Profile) == profile.Compromised {
fmt.Fprintf(stdout, "warning: the profile %s is compromised: the content of this capsule may have been read before its date (spec §71)\n",
result.Profile.ID)
}
}
return inspectErr
}
type resolveView struct {
DateKey string `json:"datekey"`
Profile string `json:"profile"`
Round uint64 `json:"round"`
Requested string `json:"requested"`
UnlockAt string `json:"unlock_at"`
}
func resolve(args []string, stdout io.Writer) error {
fs := newFlags("datekey resolve")
at := fs.String("at", "", "instant, RFC 3339")
if err := parse(fs, args); err != nil {
return err
}
t, err := parseTime(*at)
if err != nil {
return err
}
p := profile.Quicknet()
d, err := datekey.Resolve(p, t)
if err != nil {
return err
}
return json.NewEncoder(stdout).Encode(resolveView{
DateKey: d.Compact(), Profile: d.ProfileID, Round: d.Round,
Requested: t.Format(time.RFC3339Nano), UnlockAt: d.UnlockAt(p).Format(time.RFC3339),
})
}
func profileHash(args []string, stdout io.Writer) error {
fs := newFlags("profile hash")
in := fs.String("in", "", "Deterministic CBOR profile file; default: the pinned Quicknet profile")
if err := parse(fs, args); err != nil {
return err
}
p := profile.Quicknet()
if *in != "" {
b, err := os.ReadFile(*in)
if err != nil {
return err
}
if p, err = profile.Decode(b); err != nil {
return err
}
}
b, err := p.CanonicalCBOR()
if err != nil {
return err
}
h, err := p.Hash()
if err != nil {
return err
}
pinned := *in == "" || hex.EncodeToString(h[:]) == profile.QuicknetProfileHash
return json.NewEncoder(stdout).Encode(map[string]any{
"profile_id": p.ID,
"profile_hash": hex.EncodeToString(h[:]),
"canonical_cbor": hex.EncodeToString(b),
"pinned": pinned,
})
}

Powered by TurnKey Linux.