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.
//
// 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
// 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
// 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
// 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 (
"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"
"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
TIME is RFC 3339 with a time zone, for example 2030-01-01T00:00:00Z.
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.
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).
-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":
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")
var ins multi
fs.Var(&ins, "in", "file or folder to encrypt (repeatable)")
out := fs.String("out", "", "new .dkc file; never overwritten")
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")
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
}
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)
}
if *out == "" || len(ins) == 0 && *comment == "" {
return errors.New("encrypt: -out, and -in or -comment, are required")
}
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()
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).")
}
sources, skipped, err := collect(ins, !*noMTime)
if err != nil {
return fmt.Errorf("encrypt: %w", err)
}
var res *capsule.Result
err = writeAtomic(*out, func(w io.Writer) error {
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)
}
}
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",
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
}
func decrypt(args []string, stdout, stderr io.Writer, now func() time.Time) error {
fs := newFlags("decrypt")
in := fs.String("in", "", ".dkc file")
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")
}
if *release != "" && len(relays) > 0 {
return errors.New("decrypt: -release and -relay are exclusive")
}
var expected []byte
if *expect != "" {
k, err := authorkey.ParsePublic(*expect)
if err != nil {
return fmt.Errorf("decrypt: -expect-author: %w", err)
}
expected = k
}
reg, err := profile.Default()
if err != nil {
return err
}
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...)
}
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 != "" {
// 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
}
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()
// 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 {
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))
}
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
}
}
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)
}
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))
}
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)
}
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
}
// 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
// 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.