|
|
package main
|
|
|
|
|
|
import (
|
|
|
"bytes"
|
|
|
"crypto/sha256"
|
|
|
"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"
|
|
|
"g.activething.com/go/DateKeys/wordkey"
|
|
|
)
|
|
|
|
|
|
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) != 28 {
|
|
|
t.Fatalf("%d frozen inspect outputs, want one per official .dkc (%d, 28)", 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"},
|
|
|
{[]string{"-policy", "time_and_key", "-words", "a", "-new-words", filepath.Join(dir, "n.txt")}, "-new-words excludes -words and -words-file"},
|
|
|
{[]string{"-policy", "time_and_key", "-new-words", words}, "already exists"},
|
|
|
{[]string{"-policy", "time_and_key", "-new-words", filepath.Join(dir, "n.txt"), "-dic", "xx"}, `no word list for "xx"`},
|
|
|
{[]string{"-policy", "time_and_key", "-new-words", filepath.Join(dir, "n.txt"), "-word-count", "5"}, "at least 6 words, not 5"},
|
|
|
{[]string{"-new-words", filepath.Join(dir, "n.txt")}, "-words, -words-file, -new-words and -dice need -policy time_and_key"},
|
|
|
{[]string{"-dice", "11111 11112 11113 11114 11115 11116"}, "-words, -words-file, -new-words and -dice need -policy time_and_key"},
|
|
|
{[]string{"-policy", "time_and_key", "-dice", "11111", "-dice-file", words}, "-dice and -dice-file are exclusive"},
|
|
|
{[]string{"-policy", "time_and_key", "-dice", "11111", "-words", "a"}, "-dice and -dice-file exclude -words, -words-file and -new-words"},
|
|
|
{[]string{"-policy", "time_and_key", "-dice-file", words, "-new-words", filepath.Join(dir, "n.txt")}, "-dice and -dice-file exclude -words, -words-file and -new-words"},
|
|
|
{[]string{"-policy", "time_and_key", "-dice", "11111 11112 11113 11114 11115"}, "at least 6 words, not 5"},
|
|
|
{[]string{"-policy", "time_and_key", "-dice", "11111 11112 11113 11114 11115 11171"}, `"11171" is not five dice: five digits from 1 to 6`},
|
|
|
{[]string{"-policy", "time_and_key", "-dice", "11111 11112 11113 11114 11115 11111"}, `the dice 11111 give "abacus" a second time; roll them again`},
|
|
|
{[]string{"-policy", "time_and_key", "-dice", "11111 11112 11113 11114 11115 11116", "-dic", "xx"}, `no word list for "xx"`},
|
|
|
} {
|
|
|
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)
|
|
|
}
|
|
|
}
|
|
|
if _, err := os.Stat(filepath.Join(dir, "n.txt")); err == nil {
|
|
|
t.Error("a refused encrypt left its words file")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Spec §62.1, rules 26 and 27: encrypt writes the recovery annex next to the
|
|
|
// .dkc, never over another file, and says what opening it years later will
|
|
|
// take; for a long horizon and time_only, it recommends time_and_key (§7.6).
|
|
|
func TestRecoveryAndNotices(t *testing.T) {
|
|
|
dir := t.TempDir()
|
|
|
in := filepath.Join(dir, "carta.txt")
|
|
|
os.WriteFile(in, []byte("para dentro de mucho"), 0o600)
|
|
|
p := profile.Quicknet()
|
|
|
genesis := time.Unix(p.GenesisTime, 0)
|
|
|
at := time.Unix(p.GenesisTime+999*3, 0).UTC().Format(time.RFC3339) // round 1000
|
|
|
dkc := filepath.Join(dir, "carta.dkc")
|
|
|
_, stderr, err := cli(t, genesis, "encrypt", "-at", at, "-in", in, "-out", dkc)
|
|
|
if err != nil {
|
|
|
t.Fatalf("encrypt: %v\n%s", err, stderr)
|
|
|
}
|
|
|
if b, err := os.ReadFile(dkc + ".recuperacion.txt"); err != nil || string(b) != datekeys.RecoveryAnnex {
|
|
|
t.Fatalf("the annex next to the .dkc: %v", err)
|
|
|
}
|
|
|
for _, want := range []string{
|
|
|
" recovery " + dkc + ".recuperacion.txt: how to open the capsule without DateKeys software (spec §79); keep it with the .dkc",
|
|
|
" to open the .dkc and the release of round 1000, which drand publishes at 2023-08-23T",
|
|
|
"an archive of releases or a cache service must have kept it (spec §50)",
|
|
|
} {
|
|
|
if !strings.Contains(stderr, want) {
|
|
|
t.Errorf("stderr lacks %q:\n%s", want, stderr)
|
|
|
}
|
|
|
}
|
|
|
if strings.Contains(stderr, "(spec §7.6)") || strings.Contains(stderr, "(spec §53)") {
|
|
|
t.Errorf("a short horizon got a warning:\n%s", stderr)
|
|
|
}
|
|
|
// time_and_key, without the annex.
|
|
|
keyed := filepath.Join(dir, "llave.dkc")
|
|
|
_, stderr, err = cli(t, genesis, "encrypt", "-at", at, "-policy", "time_and_key", "-dkk", filepath.Join(dir, "llave.dkk"), "-no-recovery", "-in", in, "-out", keyed)
|
|
|
if err != nil {
|
|
|
t.Fatalf("encrypt -no-recovery: %v\n%s", err, stderr)
|
|
|
}
|
|
|
if _, err := os.Stat(keyed + ".recuperacion.txt"); err == nil || strings.Contains(stderr, "recovery ") {
|
|
|
t.Error("-no-recovery wrote the annex")
|
|
|
}
|
|
|
if !strings.Contains(stderr, " to open the .dkc, one of its credentials and the release of round 1000") {
|
|
|
t.Errorf("stderr:\n%s", stderr)
|
|
|
}
|
|
|
// A file where the annex goes: nothing is written.
|
|
|
taken := filepath.Join(dir, "otra.dkc")
|
|
|
os.WriteFile(taken+".recuperacion.txt", []byte("mine"), 0o600)
|
|
|
if _, _, err := cli(t, genesis, "encrypt", "-at", at, "-in", in, "-out", taken); err == nil || !strings.Contains(err.Error(), "already exists") {
|
|
|
t.Errorf("encrypt over an annex: %v", err)
|
|
|
}
|
|
|
if _, err := os.Stat(taken); err == nil {
|
|
|
t.Error("a refused encrypt wrote its .dkc")
|
|
|
}
|
|
|
// A long horizon: the warning of §53, and the note of §7.6 for time_only.
|
|
|
far := genesis.Add(400 * 24 * time.Hour).Format(time.RFC3339)
|
|
|
_, stderr, err = cli(t, genesis, "encrypt", "-at", far, "-in", in, "-out", filepath.Join(dir, "lejos.dkc"))
|
|
|
if err != nil || !strings.Contains(stderr, "(spec §53)") || !strings.Contains(stderr, "note: for a horizon this long, or a valuable content, -policy time_and_key adds a credential") {
|
|
|
t.Errorf("a long horizon, time_only: %v\n%s", err, stderr)
|
|
|
}
|
|
|
_, stderr, err = cli(t, genesis, "encrypt", "-at", far, "-policy", "time_and_key", "-dkk", filepath.Join(dir, "lejos2.dkk"), "-in", in, "-out", filepath.Join(dir, "lejos2.dkc"))
|
|
|
if err != nil || !strings.Contains(stderr, "(spec §53)") || strings.Contains(stderr, "(spec §7.6)") {
|
|
|
t.Errorf("a long horizon, time_and_key: %v\n%s", err, stderr)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Spec §71: a profile that is not active writes no capsule, and a
|
|
|
// compromised one gets a warning when a capsule of it is opened or inspected.
|
|
|
func TestProfileStatus(t *testing.T) {
|
|
|
saved := profileStatus
|
|
|
t.Cleanup(func() { profileStatus = saved })
|
|
|
dir := t.TempDir()
|
|
|
in := filepath.Join(dir, "carta.txt")
|
|
|
os.WriteFile(in, []byte("abierta"), 0o600)
|
|
|
p := profile.Quicknet()
|
|
|
genesis := time.Unix(p.GenesisTime, 0)
|
|
|
at := time.Unix(p.GenesisTime+999*3, 0).UTC().Format(time.RFC3339) // round 1000
|
|
|
dkc := filepath.Join(dir, "carta.dkc")
|
|
|
profileStatus = func(*profile.Profile) profile.Status { return profile.ReadOnly }
|
|
|
if _, _, err := cli(t, genesis, "encrypt", "-at", at, "-in", in, "-out", dkc); err == nil ||
|
|
|
err.Error() != "encrypt: the profile datekeys:quicknet:v1 is read-only: no new capsule is written with it (spec §71)" {
|
|
|
t.Fatalf("encrypt with a read-only profile: %v", err)
|
|
|
}
|
|
|
if _, err := os.Stat(dkc); err == nil {
|
|
|
t.Fatal("a read-only profile wrote a capsule")
|
|
|
}
|
|
|
profileStatus = saved
|
|
|
if _, stderr, err := cli(t, genesis, "encrypt", "-at", at, "-in", in, "-out", dkc); err != nil {
|
|
|
t.Fatalf("encrypt: %v\n%s", err, stderr)
|
|
|
}
|
|
|
const warning = "warning: the profile datekeys:quicknet:v1 is compromised: the content of this capsule may have been read before its date (spec §71)"
|
|
|
if stdout, _, err := cli(t, later, "inspect", "-in", dkc); err != nil || strings.Contains(stdout, warning) {
|
|
|
t.Errorf("inspect with an active profile: %v\n%s", err, stdout)
|
|
|
}
|
|
|
profileStatus = func(*profile.Profile) profile.Status { return profile.Compromised }
|
|
|
if stdout, _, err := cli(t, later, "inspect", "-in", dkc); err != nil || !strings.Contains(stdout, warning) {
|
|
|
t.Errorf("inspect with a compromised profile: %v\n%s", err, stdout)
|
|
|
}
|
|
|
if _, stderr, err := cli(t, later, "decrypt", "-in", dkc, "-out", filepath.Join(dir, "abierta"), "-relay", relay(t)); err != nil || !strings.Contains(stderr, warning) {
|
|
|
t.Errorf("decrypt with a compromised profile: %v\n%s", err, stderr)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// The words of dice, as the list numbered for dice gives them: the capsule
|
|
|
// opens with them.
|
|
|
func TestDice(t *testing.T) {
|
|
|
dir := t.TempDir()
|
|
|
in := filepath.Join(dir, "carta.txt")
|
|
|
os.WriteFile(in, []byte("abierta con dados"), 0o600)
|
|
|
p := profile.Quicknet()
|
|
|
unlock := time.Unix(p.GenesisTime+999*3, 0).UTC() // round 1000
|
|
|
dkc := filepath.Join(dir, "carta.dkc")
|
|
|
dice := filepath.Join(dir, "dados.txt")
|
|
|
os.WriteFile(dice, []byte("35214 11111\n64253 11112 11113\n66666 11121\n"), 0o600)
|
|
|
_, stderr, err := cli(t, time.Unix(p.GenesisTime, 0), "encrypt", "-at", unlock.Format(time.RFC3339), "-policy", "time_and_key",
|
|
|
"-dice-file", dice, "-dic", "es", "-in", in, "-out", dkc)
|
|
|
if err != nil {
|
|
|
t.Fatalf("encrypt: %v\n%s", err, stderr)
|
|
|
}
|
|
|
const words = "glaciar abad tocar abadía abandonar útil abdomen"
|
|
|
if !strings.Contains(stderr, "words "+words+": the 7 words of the dice in the list es, 90 bits; keep these words") {
|
|
|
t.Errorf("stderr: %s", stderr)
|
|
|
}
|
|
|
out := filepath.Join(dir, "abierta")
|
|
|
if _, stderr, err := cli(t, later, "decrypt", "-in", dkc, "-out", out, "-words", "Glaciar abad tocar abadia abandonar util abdomen", "-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 dados" {
|
|
|
t.Fatalf("carta.txt = %q, %v", b, err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// datekeys wordlist writes the list numbered for dice, and its SHA-256: for
|
|
|
// en, the file of the EFF.
|
|
|
func TestWordList(t *testing.T) {
|
|
|
for lang, want := range map[string]string{
|
|
|
"en": "addd35536511597a02fa0a9ff1e5284677b8883b83e986e43f15a3db996b903e",
|
|
|
"es": "611f779a33df74587e9dfb486bb0185a9dfd4d1384e75993a21247ad31cefddb",
|
|
|
} {
|
|
|
stdout, stderr, err := cli(t, time.Now(), "wordlist", "-dic", lang)
|
|
|
if err != nil {
|
|
|
t.Fatalf("wordlist -dic %s: %v", lang, err)
|
|
|
}
|
|
|
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(stdout))); got != want {
|
|
|
t.Errorf("wordlist -dic %s: SHA-256 %s, want %s", lang, got, want)
|
|
|
}
|
|
|
if stderr != "the list "+lang+" numbered for dice, 7776 words, SHA-256 "+want+"\n" {
|
|
|
t.Errorf("wordlist -dic %s: stderr %q", lang, stderr)
|
|
|
}
|
|
|
}
|
|
|
if stdout, _, err := cli(t, time.Now(), "wordlist"); err != nil || !strings.HasPrefix(stdout, "11111\tabacus\n11112\tabdomen\n") {
|
|
|
t.Errorf("wordlist: %.40q, %v", stdout, err)
|
|
|
}
|
|
|
if _, _, err := cli(t, time.Now(), "wordlist", "-dic", "xx"); err == nil || !strings.Contains(err.Error(), `wordlist: wordkey: no word list for "xx"`) {
|
|
|
t.Errorf("wordlist -dic xx: %v", err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
func TestNewWords(t *testing.T) {
|
|
|
dir := t.TempDir()
|
|
|
in := filepath.Join(dir, "carta.txt")
|
|
|
os.WriteFile(in, []byte("abierta con palabras al azar"), 0o600)
|
|
|
p := profile.Quicknet()
|
|
|
unlock := time.Unix(p.GenesisTime+999*3, 0).UTC() // round 1000
|
|
|
dkc := filepath.Join(dir, "carta.dkc")
|
|
|
words := filepath.Join(dir, "palabras.txt")
|
|
|
_, stderr, err := cli(t, time.Unix(p.GenesisTime, 0), "encrypt", "-at", unlock.Format(time.RFC3339), "-policy", "time_and_key",
|
|
|
"-new-words", words, "-word-count", "8", "-in", in, "-out", dkc)
|
|
|
if err != nil {
|
|
|
t.Fatalf("encrypt: %v\n%s", err, stderr)
|
|
|
}
|
|
|
// The list of the EFF by default.
|
|
|
if !strings.Contains(stderr, "words "+words+": 8 words of the list en, 103 bits; keep them secret") {
|
|
|
t.Errorf("stderr: %s", stderr)
|
|
|
}
|
|
|
inList := func(lang, file string, n int) {
|
|
|
t.Helper()
|
|
|
b, err := os.ReadFile(file)
|
|
|
if err != nil {
|
|
|
t.Fatal(err)
|
|
|
}
|
|
|
list, _ := wordkey.List(lang)
|
|
|
known := map[string]bool{}
|
|
|
for _, w := range list {
|
|
|
known[w] = true
|
|
|
}
|
|
|
drawn := strings.Fields(string(b))
|
|
|
if len(drawn) != n || !strings.HasSuffix(string(b), "\n") {
|
|
|
t.Fatalf("words file %q", b)
|
|
|
}
|
|
|
for _, w := range drawn {
|
|
|
if !known[w] {
|
|
|
t.Errorf("%q is not in the list %s", w, lang)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
inList("en", words, 8)
|
|
|
// The Spanish list with -dic es.
|
|
|
spanish := filepath.Join(dir, "espanol.txt")
|
|
|
_, stderr, err = cli(t, time.Unix(p.GenesisTime, 0), "encrypt", "-at", unlock.Format(time.RFC3339), "-policy", "time_and_key",
|
|
|
"-new-words", spanish, "-dic", "es", "-in", in, "-out", filepath.Join(dir, "espanol.dkc"))
|
|
|
if err != nil || !strings.Contains(stderr, "words "+spanish+": 7 words of the list es, 90 bits;") {
|
|
|
t.Fatalf("encrypt -dic es: %v\n%s", err, stderr)
|
|
|
}
|
|
|
inList("es", spanish, 7)
|
|
|
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 al azar" {
|
|
|
t.Fatalf("carta.txt = %q, %v", b, err)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 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())
|
|
|
}
|
|
|
}
|