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_test.go

662 lines
27 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

package main
import (
"bytes"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
"filippo.io/age"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/inspectview"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/profile"
)
const fixtures = "../../testdata/fixtures"
// relay serves the known Quicknet releases like a drand HTTP relay.
func relay(t *testing.T) string {
t.Helper()
p := profile.Quicknet()
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
for _, round := range testkit.Rounds {
if r.URL.Path == fmt.Sprintf("/v2/chains/%s/rounds/%d", p.ChainHashHex(), round) {
fmt.Fprintf(w, `{"round":%d,"signature":"%s"}`, round, hex.EncodeToString(testkit.Release(round).Signature))
return
}
}
http.NotFound(w, r)
}))
t.Cleanup(s.Close)
return s.URL
}
func cli(t *testing.T, now time.Time, args ...string) (string, string, error) {
t.Helper()
var out, errOut bytes.Buffer
err := run(args, &out, &errOut, func() time.Time { return now })
return out.String(), errOut.String(), err
}
var later = time.Date(2026, 9, 25, 12, 0, 0, 0, time.UTC)
// joined undoes the rows of writeVerdicts: each row after the first of a line
// goes back after the space that its break dropped. No line of the tests
// breaks inside a word at 80 columns.
func joined(s string) string { return strings.ReplaceAll(s, "\n"+contMark, " ") }
// The lines of the reader break at the last space that fits, behind the mark
// of a continuation; a word longer than a row breaks inside; and the
// indentation of a line is not a place to break it.
func TestRows(t *testing.T) {
for _, c := range []struct {
line string
first, rest int
want []string
}{
{"abc def ghi", 20, 20, []string{"abc def ghi"}},
{"abc def ghi", 7, 7, []string{"abc def", "ghi"}},
{"abc def ghi", 6, 6, []string{"abc", "def", "ghi"}},
{"abcdefghij k", 4, 3, []string{"abcd", "efg", "hij", "k"}},
{" abcdefgh", 5, 5, []string{" abc", "defgh"}},
{"ñañañaña", 4, 4, []string{"ña", "ña", "ña", "ña"}},
} {
if got := rows(c.line, c.first, c.rest); !reflect.DeepEqual(got, c.want) {
t.Errorf("rows(%q, %d, %d) = %q, want %q", c.line, c.first, c.rest, got, c.want)
}
}
var b strings.Builder
writeVerdicts(&b, []string{"Firmado con la clave que guardaste como Mamá."}, 20)
if got := b.String(); got != "Firmado con la\n"+contMark+"clave que\n"+contMark+"guardaste\n"+contMark+"como Mamá.\n" {
t.Errorf("writeVerdicts at 20 columns:\n%s", got)
}
}
func TestOutputNotPublishedOnFailureOrOverwrite(t *testing.T) {
dir := t.TempDir()
out := filepath.Join(dir, "output")
err := writeAtomic(out, func(w io.Writer) error {
_, _ = w.Write([]byte("partial plaintext"))
return errors.New("invalid authentication tag")
})
if err == nil {
t.Fatal("expected failure")
}
if _, err := os.Stat(out); !errors.Is(err, os.ErrNotExist) {
t.Fatal("published partial output")
}
if err := os.WriteFile(out, []byte("keep me"), 0o600); err != nil {
t.Fatal(err)
}
if err := writeAtomic(out, func(io.Writer) error { t.Fatal("should not run"); return nil }); err == nil {
t.Fatal("overwrote output")
}
if b, _ := os.ReadFile(out); string(b) != "keep me" {
t.Fatal("output changed")
}
if files, _ := filepath.Glob(filepath.Join(dir, ".datekeys-*")); len(files) != 0 {
t.Fatal("temporary file left behind")
}
if err := copyExclusive(out, out); err == nil {
t.Fatal("exclusive copy replaced a file")
}
}
func TestDecryptFixtures(t *testing.T) {
url := relay(t)
for _, tc := range []struct{ name, dkk string }{
{"time_only", ""},
{"time_only_extensions", ""},
{"empty_payload", ""},
{"time_and_key_portable", "time_and_key_portable.dkk"},
{"format2_time_only", ""},
{"format2_time_only_bloque256", ""},
{"format2_empty_payload", ""},
{"format2_time_only_extensions", ""},
{"format2_time_and_key_portable", "format2_time_and_key_portable.dkk"},
{"format2_time_and_key_recipients", "format2_time_and_key_recipients.dkk"},
} {
t.Run(tc.name, func(t *testing.T) {
out := filepath.Join(t.TempDir(), "plain")
args := []string{"decrypt", "-in", filepath.Join(fixtures, tc.name+".dkc"), "-out", out, "-relay", url}
if tc.dkk != "" {
args = append(args, "-dkk", filepath.Join(fixtures, tc.dkk))
}
_, stderr, err := cli(t, later, args...)
if err != nil {
t.Fatalf("%v\n%s", err, stderr)
}
// Spec §70: the format is reported, with what format 1 reveals.
format1 := !strings.HasPrefix(tc.name, "format2_")
if strings.Contains(stderr, "format 1 does not hide") != format1 {
t.Fatalf("report:\n%s", stderr)
}
got, _ := os.ReadFile(out)
want, _ := os.ReadFile(filepath.Join(fixtures, tc.name+".plaintext"))
if !bytes.Equal(got, want) {
t.Fatal("plaintext differs")
}
})
}
}
// Spec §56, §67: the format 3 fixtures open through the CLI into a new
// folder, with their mtimes, and the CLI shows their verdicts first and
// last (spec §29.7).
func TestDecryptFormat3Fixtures(t *testing.T) {
url := relay(t)
for _, tc := range []struct{ name, dkk string }{
{"format3_single", ""},
{"format3_tree", ""},
{"format3_comment_only", ""},
{"format3_time_and_key_portable", "format3_time_and_key_portable.dkk"},
{"format3_seal_unsupported", ""},
{"format3_unsigned", ""},
{"format3_signed", ""},
{"format3_signed_cms", ""},
{"format3_sealed", ""},
{"format3_note", ""},
} {
t.Run(tc.name, func(t *testing.T) {
var f testkit.DKCFixture
if err := testkit.ReadJSON(filepath.Join(fixtures, tc.name+".json"), &f); err != nil {
t.Fatal(err)
}
body, err := os.ReadFile(filepath.Join(fixtures, f.PlaintextFile))
if err != nil {
t.Fatal(err)
}
out := filepath.Join(t.TempDir(), "out")
args := []string{"decrypt", "-in", filepath.Join(fixtures, f.File), "-out", out, "-relay", url}
if tc.dkk != "" {
args = append(args, "-dkk", filepath.Join(fixtures, tc.dkk))
}
stdout, stderr, err := cli(t, later, args...)
if err != nil {
t.Fatalf("%v\n%s", err, stderr)
}
for _, file := range f.Files {
name := filepath.Join(out, filepath.FromSlash(file.Path))
got, err := os.ReadFile(name)
if err != nil || !bytes.Equal(got, body[f.ContentOffset+file.Start:f.ContentOffset+file.End]) {
t.Errorf("%s: %v", file.Path, err)
}
if info, err := os.Stat(name); file.MTime != nil && (err != nil || info.ModTime().Unix() != int64(*file.MTime)) {
t.Errorf("%s: mtime %v", file.Path, err)
}
}
if _, err := os.Lstat(out); len(f.Files) == 0 && !errors.Is(err, os.ErrNotExist) {
t.Error("a capsule without files created its folder")
}
lines := strings.Split(strings.TrimSuffix(joined(stdout), "\n"), "\n")
n := len(f.Verdicts.Lines)
if len(lines) < 2*n || !reflect.DeepEqual(lines[:n], f.Verdicts.Lines) || !reflect.DeepEqual(lines[len(lines)-n:], f.Verdicts.Lines) {
t.Errorf("the verdicts are not first and last:\n%s", stdout)
}
})
}
}
func TestDecryptWithIdentityFile(t *testing.T) {
var f testkit.DKCFixture
if err := testkit.ReadJSON(filepath.Join(fixtures, "time_and_key_recipients.json"), &f); err != nil {
t.Fatal(err)
}
dir := t.TempDir()
key := filepath.Join(dir, "key.txt")
os.WriteFile(key, []byte("# test identity\n"+f.Identities[1]+"\n"), 0o600)
out := filepath.Join(dir, "plain")
if _, stderr, err := cli(t, later, "decrypt", "-in", filepath.Join(fixtures, f.File), "-out", out, "-identity", key, "-relay", relay(t)); err != nil {
t.Fatalf("%v\n%s", err, stderr)
}
}
func TestDecryptFailuresLeaveNothing(t *testing.T) {
dir := t.TempDir()
b, _ := os.ReadFile(filepath.Join(fixtures, "time_only.dkc"))
bad := filepath.Join(dir, "bad.dkc")
b[len(b)-1] ^= 1
os.WriteFile(bad, b, 0o600)
out := filepath.Join(dir, "plain")
_, _, err := cli(t, later, "decrypt", "-in", bad, "-out", out, "-relay", relay(t))
if !errors.Is(err, datekeys.ErrIntegrity) {
t.Fatalf("got %v", err)
}
if entries, _ := os.ReadDir(dir); len(entries) != 1 {
t.Fatalf("left files behind: %v", entries)
}
// time_and_key without credentials fails before contacting any relay.
_, _, err = cli(t, later, "decrypt", "-in", filepath.Join(fixtures, "time_and_key_portable.dkc"), "-out", out, "-relay", "http://127.0.0.1:1")
if !errors.Is(err, datekeys.ErrAccessRequired) {
t.Fatalf("got %v", err)
}
}
// Spec §63 step 9.a, §69.1: the CLI hands the .dkk to capsule.Open still
// encoded, so its errors come at step 9 of a time_and_key capsule, after any
// failure of steps 1 to 8, and a time_only capsule ignores it.
func TestDecryptAccessKeyOrder(t *testing.T) {
dir := t.TempDir()
notDKK := filepath.Join(dir, "not.dkk")
os.WriteFile(notDKK, []byte("not a .dkk"), 0o600)
b, _ := os.ReadFile(filepath.Join(fixtures, "time_and_key_portable.dkc"))
b[5] = 1
flags := filepath.Join(dir, "flags.dkc")
os.WriteFile(flags, b, 0o600)
for _, tc := range []struct {
name, in string
want error
}{
{"time_and_key and bytes that are not a .dkk", filepath.Join(fixtures, "time_and_key_portable.dkc"), datekeys.ErrInvalidMagic},
{"FLAGS 1 and bytes that are not a .dkk", flags, datekeys.ErrInvalidFlags},
} {
_, _, err := cli(t, later, "decrypt", "-in", tc.in, "-out", filepath.Join(dir, "plain"), "-dkk", notDKK, "-relay", "http://127.0.0.1:1")
if !errors.Is(err, tc.want) {
t.Errorf("%s: got %v, want %s", tc.name, err, datekeys.Code(tc.want))
}
}
out := filepath.Join(dir, "plain")
if _, stderr, err := cli(t, later, "decrypt", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", out, "-dkk", notDKK, "-relay", relay(t)); err != nil {
t.Fatalf("time_only and bytes that are not a .dkk: %v\n%s", err, stderr)
}
}
func TestEncryptDecryptRoundTrip(t *testing.T) {
dir := t.TempDir()
in := filepath.Join(dir, "secret.txt")
os.WriteFile(in, []byte("round trip through the CLI"), 0o600)
fotos := filepath.Join(dir, "fotos")
os.MkdirAll(filepath.Join(fotos, "sub"), 0o700)
os.MkdirAll(filepath.Join(fotos, "__MACOSX"), 0o700)
os.WriteFile(filepath.Join(fotos, "a.jpg"), []byte("jpeg"), 0o600)
os.WriteFile(filepath.Join(fotos, "sub", "b.txt"), []byte("b"), 0o600)
os.WriteFile(filepath.Join(fotos, ".DS_Store"), []byte("x"), 0o600)
os.WriteFile(filepath.Join(fotos, "__MACOSX", "._a.jpg"), []byte("x"), 0o600)
old := time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC)
os.Chtimes(in, old, old)
p := profile.Quicknet()
unlock := time.Unix(p.GenesisTime+999*3, 0).UTC() // round 1000
genesis := time.Unix(p.GenesisTime, 0)
x, _ := age.GenerateX25519Identity()
key := filepath.Join(dir, "x.txt")
os.WriteFile(key, []byte(x.String()+"\n"), 0o600)
dkc, dkk := filepath.Join(dir, "s.dkc"), filepath.Join(dir, "s.dkk")
_, stderr, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-in", in, "-in", fotos, "-out", dkc,
"-comment", "Hola\tmundo", "-author", "Ana", "-policy", "time_and_key", "-recipient", x.Recipient().String(), "-dkk", dkk)
if err != nil {
t.Fatalf("%v\n%s", err, stderr)
}
if !strings.Contains(stderr, "round 1000") || !strings.Contains(stderr, "format 3: 3 files, ") ||
!strings.Contains(stderr, ".DS_Store, which the system creates on its own") || !strings.Contains(stderr, "__MACOSX, which the system") ||
strings.Contains(stderr, "not post-quantum") {
t.Fatalf("unexpected report:\n%s", stderr)
}
if _, _, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-in", in, "-out", dkc); err == nil {
t.Fatal("overwrote an existing capsule")
}
// Spec §29.1: the padding rule is one of the two, never none.
small := filepath.Join(dir, "small.dkc")
if _, stderr, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-in", in, "-out", small, "-padding", "bloque256", "-no-mtime"); err != nil ||
!strings.Contains(stderr, "(bloque256)") {
t.Fatalf("-padding bloque256: %v\n%s", err, stderr)
}
if _, _, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-in", in, "-out", filepath.Join(dir, "none.dkc"), "-padding", "none"); err == nil {
t.Fatal("encrypted without a padding rule")
}
stdout, _, err := cli(t, later, "inspect", "-in", dkc, "-json")
if err != nil {
t.Fatal(err)
}
var v inspectview.View
if err := json.Unmarshal([]byte(stdout), &v); err != nil || !v.Valid || v.Round != 1000 || v.AccessPolicy != "time_and_key" || v.Format != 3 {
t.Fatalf("inspect: %+v %v", v, err)
}
for i, extra := range [][]string{{"-dkk", dkk}, {"-identity", key}} {
out := filepath.Join(dir, fmt.Sprintf("out%d", i))
args := append([]string{"decrypt", "-in", dkc, "-out", out, "-relay", relay(t)}, extra...)
stdout, stderr, err := cli(t, later, args...)
if err != nil {
t.Fatalf("%v\n%s", err, stderr)
}
for name, want := range map[string]string{"secret.txt": "round trip through the CLI", "fotos/a.jpg": "jpeg", "fotos/sub/b.txt": "b"} {
if b, _ := os.ReadFile(filepath.Join(out, filepath.FromSlash(name))); string(b) != want {
t.Errorf("%s: %q", name, b)
}
}
if entries, _ := os.ReadDir(out); len(entries) != 2 {
t.Errorf("%d entries in the folder, want fotos and secret.txt", len(entries))
}
if info, err := os.Stat(filepath.Join(out, "secret.txt")); err != nil || !info.ModTime().Equal(old) {
t.Errorf("mtime of secret.txt: %v", err)
}
// Spec §29.7: the verdicts, the declared author and the comment, as
// text of the creator, the paths, and the verdicts again.
want := "Sin firma de autor.\n" + authorLabel + "\n│ Ana\n┌ " + commentTitle + "\n│ Hola mundo\n└\n" +
"Ficheros escritos en " + out + " (3):\n│ fotos/a.jpg\n│ fotos/sub/b.txt\n│ secret.txt\nSin firma de autor.\n"
if stdout != want {
t.Errorf("presentation:\n%s\nwant:\n%s", stdout, want)
}
}
// The folder exists: nothing is requested and nothing changes.
out := filepath.Join(dir, "out0")
if _, _, err := cli(t, later, "decrypt", "-in", dkc, "-out", out, "-dkk", dkk, "-relay", "http://127.0.0.1:1"); err == nil || !strings.Contains(err.Error(), "already exists") {
t.Fatalf("decrypted into an existing folder: %v", err)
}
// -no-mtime: the file gets the time of its extraction.
out = filepath.Join(dir, "small")
if _, stderr, err := cli(t, later, "decrypt", "-in", small, "-out", out, "-relay", relay(t)); err != nil {
t.Fatalf("%v\n%s", err, stderr)
}
if info, err := os.Stat(filepath.Join(out, "secret.txt")); err != nil || info.ModTime().Equal(old) {
t.Errorf("-no-mtime kept the mtime: %v", err)
}
}
// Spec §56: a format 3 capsule that fails leaves no folder, and one without
// files creates none.
func TestDecryptFormat3LeavesNothing(t *testing.T) {
dir := t.TempDir()
p := profile.Quicknet()
unlock := time.Unix(p.GenesisTime+999*3, 0).UTC()
genesis := time.Unix(p.GenesisTime, 0)
in := filepath.Join(dir, "a.txt")
os.WriteFile(in, []byte("a"), 0o600)
dkc, note := filepath.Join(dir, "a.dkc"), filepath.Join(dir, "note.dkc")
if _, stderr, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-in", in, "-out", dkc); err != nil {
t.Fatalf("%v\n%s", err, stderr)
}
if _, stderr, err := cli(t, genesis, "encrypt", "-at", unlock.Format(time.RFC3339), "-comment", "Solo un comentario", "-out", note); err != nil {
t.Fatalf("%v\n%s", err, stderr)
}
b, _ := os.ReadFile(dkc)
b[len(b)-1] ^= 1
bad := filepath.Join(dir, "bad.dkc")
os.WriteFile(bad, b, 0o600)
out := filepath.Join(dir, "out")
if _, _, err := cli(t, later, "decrypt", "-in", bad, "-out", out, "-relay", relay(t)); !errors.Is(err, datekeys.ErrIntegrity) {
t.Fatalf("got %v", err)
}
if _, err := os.Lstat(out); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("the folder of a failed capsule remains: %v", err)
}
// A folder whose parent does not exist fails before any request: the
// relay cannot be reached.
missing := filepath.Join(dir, "missing", "out")
if _, _, err := cli(t, later, "decrypt", "-in", dkc, "-out", missing, "-relay", "http://127.0.0.1:1"); err == nil || !strings.Contains(err.Error(), "is not a folder") {
t.Fatalf("a folder without its parent: %v", err)
}
stdout, stderr, err := cli(t, later, "decrypt", "-in", note, "-out", out, "-relay", relay(t))
if err != nil || !strings.Contains(stdout, "│ Solo un comentario") || !strings.Contains(stderr, "no files") {
t.Fatalf("%v\n%s\n%s", err, stdout, stderr)
}
if _, err := os.Lstat(out); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("a capsule without files created its folder: %v", err)
}
}
// Spec §62.1 rule 15: a path that breaks a rule is refused with the rule and
// the character, and folders are walked without following links.
func TestEncryptRefusesPaths(t *testing.T) {
dir := t.TempDir()
p := profile.Quicknet()
at := time.Unix(p.GenesisTime+999*3, 0).UTC().Format(time.RFC3339)
genesis := time.Unix(p.GenesisTime, 0)
bidi := filepath.Join(dir, "a\u202eb.txt")
if err := os.WriteFile(bidi, []byte("x"), 0o600); err != nil {
t.Fatal(err)
}
_, _, err := cli(t, genesis, "encrypt", "-at", at, "-in", bidi, "-out", filepath.Join(dir, "1.dkc"))
if err == nil || !strings.Contains(err.Error(), "R4: segment 1: invisible U+202E") {
t.Fatalf("got %v", err)
}
linked := filepath.Join(dir, "linked")
os.Mkdir(linked, 0o700)
if err := os.Symlink(bidi, filepath.Join(linked, "link")); err != nil {
t.Skipf("no symbolic links here: %v", err)
}
if _, _, err := cli(t, genesis, "encrypt", "-at", at, "-in", linked, "-out", filepath.Join(dir, "2.dkc")); err == nil || !strings.Contains(err.Error(), "is not a regular file") {
t.Fatalf("got %v", err)
}
}
// Spec §29.7: every line of the creator goes in pieces of at most W - 3
// columns behind the prefix, counting 2 for any code point that is not
// printable ASCII; TABs of the comment go to the next multiple of 8; the
// verdicts come first and last, in rows of at most W - 3 columns; risky names
// get a warning.
func TestPresent(t *testing.T) {
o := &capsule.Opened{
Verdicts: capsule.Verdicts{Signature: capsule.VerdictUnreadable, Seal: capsule.VerdictUnreadable},
Head: &capsule.Head{
Author: strings.Repeat("ñ", 12),
Comment: "a\tb\n\n" + strings.Repeat("x", 40),
Files: []capsule.File{{Path: "Informe.LNK"}, {Path: ".GIT/config"}, {Path: "-rf"}, {Path: "setup.Exe"}, {Path: "fotos/Desktop.ini"}, {Path: "nota.txt"}},
},
}
var b bytes.Buffer
present(&b, o, "DIR", 20)
out := joined(b.String())
lines := strings.Split(strings.TrimSuffix(out, "\n"), "\n")
x := capsule.VerdictUnreadable.Text()
if lines[0] != x || lines[len(lines)-1] != x {
t.Errorf("the verdict is not first and last:\n%s", b.String())
}
verdictRows := strings.Count(b.String(), contMark) / 2
for i, l := range strings.Split(strings.TrimSuffix(b.String(), "\n"), "\n") {
w := 0
for _, r := range l {
w += runeWidth(r)
}
rest, creator := strings.CutPrefix(l, prefix)
switch {
case creator && (w > 20 || rest == "" && l != prefix):
t.Errorf("piece %q of %d columns", rest, w)
case i <= verdictRows && w > 20-prefixWidth:
t.Errorf("row %q of the verdicts of %d columns", l, w)
}
}
for _, want := range []string{
"│ ññññññññ\n│ ññññ\n", // 12 × 2 columns in pieces of 16
"│ a b\n│ \n│ xxxxxxxxxxxxxxxxx\n", // the TAB to column 8, an empty line
"│ Informe.LNK\n aviso: es un acceso directo de Windows",
"│ .GIT/config\n aviso: está dentro de una carpeta .git",
"│ -rf\n aviso: un nombre que empieza por '-'",
"│ setup.Exe\n aviso: es un programa o un script",
"│ fotos/Desktop.ini\n aviso: es la configuración de una carpeta de Windows",
"│ nota.txt\n" + x,
} {
if !strings.Contains(out, want) {
t.Errorf("missing %q in:\n%s", want, out)
}
}
if pieces("", 17)[0] != "" || len(pieces("ab", 1)) != 2 {
t.Error("an empty line is one piece, and every piece holds a code point")
}
}
func TestInspectReportsFailures(t *testing.T) {
b, _ := os.ReadFile(filepath.Join(fixtures, "time_only.dkc"))
b = bytes.Replace(b, []byte("-> tlock 1000 "), []byte("-> tlock 1001 "), 1)
path := filepath.Join(t.TempDir(), "bad.dkc")
os.WriteFile(path, b, 0o600)
stdout, _, err := cli(t, later, "inspect", "-in", path)
if !errors.Is(err, datekeys.ErrRoundMismatch) || !strings.Contains(stdout, "[FAIL] step 8") {
t.Fatalf("%v\n%s", err, stdout)
}
stdout, _, err = cli(t, later, "inspect", "-in", filepath.Join(fixtures, "time_only.dkc"))
if err != nil || !strings.Contains(stdout, "valid before unlock") {
t.Fatalf("%v\n%s", err, stdout)
}
}
// The frozen inspect outputs (testdata/fixtures/<name>.inspect.json) are
// exactly what "datekeys inspect -json -in <name>.dkc" prints in the fixture
// directory.
func TestInspectJSONGoldens(t *testing.T) {
names, err := filepath.Glob(filepath.Join(fixtures, "*.inspect.json"))
if err != nil {
t.Fatal(err)
}
dkcs, err := filepath.Glob(filepath.Join(fixtures, "*.dkc"))
if err != nil {
t.Fatal(err)
}
if len(names) != len(dkcs) || len(dkcs) != 26 {
t.Fatalf("%d frozen inspect outputs, want one per official .dkc (%d, 26)", len(names), len(dkcs))
}
t.Chdir(fixtures)
for _, path := range names {
name := strings.TrimSuffix(filepath.Base(path), ".inspect.json")
t.Run(name, func(t *testing.T) {
want, err := os.ReadFile(name + ".inspect.json")
if err != nil {
t.Fatal(err)
}
stdout, _, err := cli(t, later, "inspect", "-json", "-in", name+".dkc")
if err != nil {
t.Fatal(err)
}
if stdout != string(want) {
t.Fatalf("output differs from %s.inspect.json:\n%s", name, stdout)
}
})
}
}
func TestResolveAndProfile(t *testing.T) {
stdout, _, err := cli(t, later, "datekey", "resolve", "-at", "2030-01-01T00:00:00Z")
if err != nil || !strings.Contains(stdout, `"round":66884212`) || !strings.Contains(stdout, `"unlock_at":"2030-01-01T00:00:00Z"`) {
t.Fatalf("%v %s", err, stdout)
}
if _, _, err := cli(t, later, "datekey", "resolve", "-at", "2030-01-01 00:00"); err == nil {
t.Fatal("accepted a time without zone")
}
stdout, _, err = cli(t, later, "profile", "hash")
if err != nil || !strings.Contains(stdout, profile.QuicknetProfileHash) || !strings.Contains(stdout, `"pinned":true`) {
t.Fatalf("%v %s", err, stdout)
}
b, _ := profile.Quicknet().CanonicalCBOR()
path := filepath.Join(t.TempDir(), "q.cbor")
os.WriteFile(path, b, 0o600)
if stdout, _, err = cli(t, later, "profile", "hash", "-in", path); err != nil || !strings.Contains(stdout, profile.QuicknetProfileHash) {
t.Fatalf("%v %s", err, stdout)
}
}
func TestUsage(t *testing.T) {
for _, args := range [][]string{nil, {"nope"}, {"datekey"}, {"profile", "x"}, {"encrypt", "-bogus"}, {"inspect", "extra"}, {"version", "extra"}} {
if _, _, err := cli(t, later, args...); err == nil {
t.Errorf("%v accepted", args)
}
}
}
// The version command names the module version, the specification and the
// toolchain; in a test the module version is unknown.
func TestVersion(t *testing.T) {
for _, arg := range []string{"version", "-version", "--version"} {
out, _, err := cli(t, later, arg)
if err != nil {
t.Fatal(err)
}
want := "datekeys " + datekeys.Version() + "\nspecification " + datekeys.SpecVersion + "\n" + runtime.Version() + " " + runtime.GOOS + "/" + runtime.GOARCH + "\n"
if out != want {
t.Errorf("%s: %q, want %q", arg, out, want)
}
}
if !strings.Contains(usage, "datekeys version") {
t.Error("the usage does not list the version command")
}
}
// Spec §53: long horizons get the harvest-now, decrypt-later warning.
func TestLongHorizonWarning(t *testing.T) {
dir := t.TempDir()
in := filepath.Join(dir, "in")
os.WriteFile(in, []byte("x"), 0o600)
for i, tc := range []struct {
after time.Duration
warn bool
}{{time.Hour, false}, {2 * 365 * 24 * time.Hour, true}} {
out := filepath.Join(dir, fmt.Sprintf("%d.dkc", i))
_, stderr, err := cli(t, later, "encrypt", "-at", later.Add(tc.after).Format(time.RFC3339), "-in", in, "-out", out)
if err != nil || strings.Contains(stderr, "not post-quantum") != tc.warn {
t.Fatalf("%s: %v: %s", tc.after, err, stderr)
}
}
}
func TestKeyOfWords(t *testing.T) {
dir := t.TempDir()
in := filepath.Join(dir, "carta.txt")
os.WriteFile(in, []byte("abierta con palabras"), 0o600)
p := profile.Quicknet()
unlock := time.Unix(p.GenesisTime+999*3, 0).UTC() // round 1000
genesis := time.Unix(p.GenesisTime, 0)
at := unlock.Format(time.RFC3339)
dkc := filepath.Join(dir, "carta.dkc")
if _, stderr, err := cli(t, genesis, "encrypt", "-at", at, "-policy", "time_and_key", "-words", "Perro luna casa verde trén mar", "-in", in, "-out", dkc); err != nil {
t.Fatalf("encrypt: %v\n%s", err, stderr)
}
words := filepath.Join(dir, "palabras.txt")
os.WriteFile(words, []byte(" perro LUNA casa\nverde tren mar\n"), 0o600)
out := filepath.Join(dir, "abierta")
if _, stderr, err := cli(t, later, "decrypt", "-in", dkc, "-out", out, "-words-file", words, "-relay", relay(t)); err != nil {
t.Fatalf("decrypt: %v\n%s", err, stderr)
}
if b, err := os.ReadFile(filepath.Join(out, "carta.txt")); err != nil || string(b) != "abierta con palabras" {
t.Fatalf("carta.txt = %q, %v", b, err)
}
if _, _, err := cli(t, later, "decrypt", "-in", dkc, "-out", filepath.Join(dir, "otra"), "-words", "gato luna casa verde tren mar", "-relay", relay(t)); err == nil {
t.Fatal("other words opened the capsule")
}
for _, tc := range []struct {
args []string
want string
}{
{[]string{"-policy", "time_and_key", "-words", "uno dos tres"}, "at least 6 different words of 3 or more letters, not 3"},
{[]string{"-policy", "time_and_key", "-words", "de la casa al mar en tren verde"}, "not 4"},
{[]string{"-policy", "time_and_key", "-words", "perro luna casa verde tren mar" + string(rune(0x200B))}, "invisible character U+200B"},
{[]string{"-words", "uno dos tres cuatro cinco seis"}, "need -policy time_and_key"},
{[]string{"-policy", "time_and_key", "-words", "a", "-words-file", words}, "are exclusive"},
} {
args := append([]string{"encrypt", "-at", at, "-in", in, "-out", filepath.Join(dir, "x.dkc")}, tc.args...)
if _, _, err := cli(t, genesis, args...); err == nil || !strings.Contains(err.Error(), tc.want) {
t.Errorf("%v: %v, want %q", tc.args, err, tc.want)
}
}
}
// Spec v0.11 §24.1: decrypt does not show a public note that breaks the rules
// of text, and says so.
func TestPresentUnusableNote(t *testing.T) {
tab := capsule.Header{Noncritical: []extension.Extension{{ID: extension.NoteID, Version: 1, Data: []byte("a\tb")}}}
o := &capsule.Opened{
Verdicts: capsule.Verdicts{Signature: capsule.VerdictNoSignature, Seal: capsule.VerdictNoSeal},
Head: &capsule.Head{Files: []capsule.File{{Path: "nota.txt"}}},
Inspection: &capsule.Inspection{Header: &tab},
}
var b bytes.Buffer
present(&b, o, "DIR", 80)
if !strings.Contains(joined(b.String()), unusableNote+"\n") || strings.Contains(b.String(), noteTitle) {
t.Errorf("an unusable note:\n%s", b.String())
}
}

Powered by TurnKey Linux.